diff --git a/eng/update-dependencies/FromStagingPipelineCommand.cs b/eng/update-dependencies/FromStagingPipelineCommand.cs index 17009a08eb..7e5ede5e77 100644 --- a/eng/update-dependencies/FromStagingPipelineCommand.cs +++ b/eng/update-dependencies/FromStagingPipelineCommand.cs @@ -244,8 +244,8 @@ public static async Task CreateAsync( var prBranch = options.CreatePrBranchName($"update-deps-int-{options.StageContainer}", buildId); var committer = options.GetCommitterIdentity(); - // Clone the repo - var git = await gitRepoFactory.CreateAndCloneAsync(remoteUrl); + // Clone the repo and configure git identity for commits + var git = await gitRepoFactory.CreateAndCloneAsync(remoteUrl, gitIdentity: committer); // Ensure the branch we want to modify exists, then check it out await git.Remote.EnsureBranchExistsAsync(targetBranch); // Create a new branch to push changes to and create a PR from diff --git a/eng/update-dependencies/Git/GitRepoHelperFactory.cs b/eng/update-dependencies/Git/GitRepoHelperFactory.cs index 318f27a6ed..85224059a8 100644 --- a/eng/update-dependencies/Git/GitRepoHelperFactory.cs +++ b/eng/update-dependencies/Git/GitRepoHelperFactory.cs @@ -23,7 +23,15 @@ internal interface IGitRepoHelperFactory /// defaults to a temporary directory. The caller is responsible for /// managing/cleaning up the temporary directory. /// - Task CreateAndCloneAsync(string repoUri, string? localCloneDir = null); + /// + /// The git identity (name and email) to configure on the cloned repository. + /// This is required for commits to succeed in environments where git is not + /// globally configured (e.g., CI/CD pipelines). + /// + Task CreateAndCloneAsync( + string repoUri, + string? localCloneDir = null, + (string Name, string Email)? gitIdentity = null); /// /// Creates an that points to an existing @@ -54,7 +62,10 @@ IServiceProvider serviceProvider private readonly IServiceProvider _serviceProvider = serviceProvider; /// - public async Task CreateAndCloneAsync(string repoUri, string? localCloneDir = null) + public async Task CreateAndCloneAsync( + string repoUri, + string? localCloneDir = null, + (string Name, string Email)? gitIdentity = null) { localCloneDir ??= Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); @@ -66,7 +77,19 @@ await _gitRepoCloner.CloneAsync( gitDirectory: null); _logger.LogInformation("Cloned '{RepoUri}' to '{LocalCloneDir}'", repoUri, localCloneDir); - return CreateFromLocal(repoUri, localCloneDir); + var gitRepoHelper = CreateFromLocal(repoUri, localCloneDir); + + if (gitIdentity is { } identity) + { + var localGitRepo = _localGitRepoFactory.Create(new NativePath(localCloneDir)); + await localGitRepo.SetConfigValue("user.name", identity.Name); + await localGitRepo.SetConfigValue("user.email", identity.Email); + _logger.LogInformation( + "Configured git identity: {Name} <{Email}>", + identity.Name, identity.Email); + } + + return gitRepoHelper; } /// diff --git a/eng/update-dependencies/Sync/SyncInternalReleaseCommand.cs b/eng/update-dependencies/Sync/SyncInternalReleaseCommand.cs index 40eb381506..b2d342eece 100644 --- a/eng/update-dependencies/Sync/SyncInternalReleaseCommand.cs +++ b/eng/update-dependencies/Sync/SyncInternalReleaseCommand.cs @@ -43,7 +43,14 @@ public override async Task ExecuteAsync(SyncInternalReleaseOptions options) $"The source branch '{options.SourceBranch}' cannot be an internal branch."); } - using var repo = await _gitRepoHelperFactory.CreateAndCloneAsync(remoteUrl); + // Get git identity if available (required for commits, optional for read-only operations) + var gitIdentity = !string.IsNullOrWhiteSpace(options.User) && !string.IsNullOrWhiteSpace(options.Email) + ? options.GetCommitterIdentity() + : ((string, string)?)null; + + using var repo = await _gitRepoHelperFactory.CreateAndCloneAsync( + remoteUrl, + gitIdentity: gitIdentity); // Verify that the source branch exists on the remote. var sourceBranchExists = await repo.Remote.RemoteBranchExistsAsync(options.SourceBranch); diff --git a/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs b/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs index 9f3a710864..352fb25012 100644 --- a/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs +++ b/tests/UpdateDependencies.Tests/SyncInternalReleaseTests.cs @@ -82,7 +82,7 @@ public async Task CreateInternalBranch() var repoMock = new Mock(); var repoFactoryMock = new Mock(); - repoFactoryMock.Setup(f => f.CreateAndCloneAsync(options.GetAzdoRepoUrl(), null)).ReturnsAsync(repoMock.Object); + repoFactoryMock.Setup(f => f.CreateAndCloneAsync(options.GetAzdoRepoUrl(), null, It.IsAny<(string, string)?>())).ReturnsAsync(repoMock.Object); // Setup: // Target branch does not exist on remote @@ -112,7 +112,7 @@ public async Task AlreadyUpToDate() // not explicitly set up in this test. var repoMock = new Mock(MockBehavior.Strict); var repoFactoryMock = new Mock(); - repoFactoryMock.Setup(f => f.CreateAndCloneAsync(options.GetAzdoRepoUrl(), null)).ReturnsAsync(repoMock.Object); + repoFactoryMock.Setup(f => f.CreateAndCloneAsync(options.GetAzdoRepoUrl(), null, It.IsAny<(string, string)?>())).ReturnsAsync(repoMock.Object); // Setup: Both target and source branches exist on remote. repoMock.Setup(r => r.Remote.RemoteBranchExistsAsync(options.TargetBranch)).ReturnsAsync(true); @@ -149,7 +149,7 @@ public async Task FastForward() repoMock.Setup(r => r.Remote).Returns(remoteRepoMock.Object); var repoFactoryMock = new Mock(); - repoFactoryMock.Setup(f => f.CreateAndCloneAsync(options.GetAzdoRepoUrl(), null)).ReturnsAsync(repoMock.Object); + repoFactoryMock.Setup(f => f.CreateAndCloneAsync(options.GetAzdoRepoUrl(), null, It.IsAny<(string, string)?>())).ReturnsAsync(repoMock.Object); // Setup: Both target and source branches exist on remote. repoMock.Setup(r => r.Remote.RemoteBranchExistsAsync(options.TargetBranch)).ReturnsAsync(true); @@ -336,7 +336,7 @@ private class GitTestScenario public GitTestScenario(string localRepoPath, string remoteRepoUrl) { RepoMock.Setup(r => r.Local.LocalPath).Returns(localRepoPath); - RepoFactoryMock.Setup(f => f.CreateAndCloneAsync(remoteRepoUrl, null)).ReturnsAsync(RepoMock.Object); + RepoFactoryMock.Setup(f => f.CreateAndCloneAsync(remoteRepoUrl, null, It.IsAny<(string, string)?>())).ReturnsAsync(RepoMock.Object); } ///