Merge pull request #1440 from akhera99/dartlab_pipeline_script

Adds a command to run the DartLab pipeline
This commit is contained in:
Ankita Khera 2024-08-26 17:20:23 -07:00 коммит произвёл GitHub
Родитель 244b5517fe 86dd49a209
Коммит 850c7ef23a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
11 изменённых файлов: 349 добавлений и 8 удалений

Просмотреть файл

@ -0,0 +1,81 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the License.txt file in the project root for more information.
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.RoslynTools.Utilities;
using Microsoft.RoslynTools.VS;
namespace Microsoft.RoslynTools.Commands;
internal static class DartTestCommand
{
private static readonly string[] s_allProductNames = VSBranchInfo.AllProducts.Where(p => p.DartLabPipelineName != null).Select(p => p.Name.ToLower()).ToArray();
private static readonly DartTestCommandDefaultHandler s_dartTestCommandHandler = new();
private static readonly Option<int> prNumber = new(["--prNumber", "-n"], "PR number") { IsRequired = true };
private static readonly Option<string> sha = new(["--sha", "-s"], "Relevant SHA") { IsRequired = false };
private static readonly Option<string> productOption = new Option<string>(new[] { "--product", "-p" }, () => "roslyn", "Which product to get info for").FromAmong(s_allProductNames);
public static Symbol GetCommand()
{
var command = new Command("dart-test",
@"Runs the dartlab pipeline for a given PR number and SHA.
It works by cloning the PR into the internal mirror and then running the dartlab pipeline from it.")
{
prNumber,
sha,
productOption,
CommonOptions.GitHubTokenOption,
CommonOptions.DevDivAzDOTokenOption,
CommonOptions.DncEngAzDOTokenOption,
CommonOptions.IsCIOption,
};
command.Handler = s_dartTestCommandHandler;
return command;
}
private class DartTestCommandDefaultHandler : ICommandHandler
{
public async Task<int> InvokeAsync(InvocationContext context)
{
var logger = context.SetupLogging();
var settings = context.ParseResult.LoadSettings(logger);
var isMissingAzDOToken = string.IsNullOrEmpty(settings.DevDivAzureDevOpsToken) || string.IsNullOrEmpty(settings.DncEngAzureDevOpsToken);
if (string.IsNullOrEmpty(settings.GitHubToken) ||
(settings.IsCI && isMissingAzDOToken))
{
logger.LogError("Missing authentication token.");
return -1;
}
var pr = context.ParseResult.GetValueForOption(prNumber);
var shaFromPR = context.ParseResult.GetValueForOption(sha);
if (shaFromPR is null)
{
logger.LogInformation("Using most recent SHA");
}
var product = context.ParseResult.GetValueForOption(productOption)!;
using var remoteConnections = new RemoteConnections(settings);
return await DartTest.DartTest.RunDartPipeline(
product,
remoteConnections,
logger,
pr,
shaFromPR).ConfigureAwait(false);
}
}
}

Просмотреть файл

@ -5,7 +5,7 @@
using System.CommandLine;
using System.CommandLine.Invocation;
using Microsoft.Extensions.Logging;
using Microsoft.RoslynTools.PRTagger;
using Microsoft.RoslynTools.Utilities;
namespace Microsoft.RoslynTools.Commands;
@ -45,9 +45,9 @@ The checking build list is created:
var logger = context.SetupLogging();
var settings = context.ParseResult.LoadSettings(logger);
var isMissingAzDOToken = string.IsNullOrEmpty(settings.DevDivAzureDevOpsToken) || string.IsNullOrEmpty(settings.DncEngAzureDevOpsToken);
if (string.IsNullOrEmpty(settings.GitHubToken) ||
string.IsNullOrEmpty(settings.DevDivAzureDevOpsToken) ||
string.IsNullOrEmpty(settings.DncEngAzureDevOpsToken))
(settings.IsCI && isMissingAzDOToken))
{
logger.LogError("Missing authentication token.");
return -1;

Просмотреть файл

@ -20,6 +20,7 @@ internal static class RootRoslynCommand
NuGetPublishCommand.GetCommand(),
CreateReleaseTagsCommand.GetCommand(),
VSBranchInfoCommand.GetCommand(),
DartTestCommand.GetCommand(),
};
command.Name = "roslyn-tools";
command.Description = "The command line tool for performing infrastructure tasks.";

Просмотреть файл

@ -0,0 +1,227 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the License.txt file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using LibGit2Sharp;
using Microsoft.Azure.Pipelines.WebApi;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.RoslynTools.Products;
using Microsoft.RoslynTools.Utilities;
using Microsoft.RoslynTools.VS;
using Microsoft.TeamFoundation.Common;
using Microsoft.TeamFoundation.SourceControl.WebApi;
using static System.Net.WebRequestMethods;
namespace Microsoft.RoslynTools.DartTest;
internal static class DartTest
{
public static async Task<int> RunDartPipeline(
string productName,
RemoteConnections remoteConnections,
ILogger logger,
int prNumber,
string? sha)
{
string? targetDirectory = null;
try
{
var cancellationToken = CancellationToken.None;
var azureBranchName = $"dart-test/{prNumber}";
var product = VSBranchInfo.AllProducts.Single(p => p.Name.Equals(productName, StringComparison.OrdinalIgnoreCase));
// If the user doesn't pass the SHA, retrieve the most recent from the PR.
if (sha is null)
{
sha = await GetLatestShaFromPullRequestAsync(product, remoteConnections.GitHubClient, prNumber, logger, cancellationToken).ConfigureAwait(false);
if (sha is null)
{
logger.LogError("Could not find a SHA for the given PR number.");
return -1;
}
}
targetDirectory = CreateDirectory(prNumber);
await PushPRToInternalAsync(product, prNumber, azureBranchName, logger, sha, targetDirectory, cancellationToken).ConfigureAwait(false);
var repositoryParams = new Dictionary<string, RepositoryResourceParameters>
{
{
"self", new RepositoryResourceParameters
{
RefName = $"refs/heads/{azureBranchName}",
Version = sha
}
}
};
var runPipelineParameters = new RunPipelineParameters
{
Resources = new RunResourcesParameters
{
},
TemplateParameters = new Dictionary<string, string> { { "prNumber", prNumber.ToString() }, { "sha", sha } }
};
await remoteConnections.DevDivConnection.TryRunPipelineAsync(product.DartLabPipelineName, repositoryParams, runPipelineParameters, logger).ConfigureAwait(false);
}
catch (Exception e)
{
logger.LogError(e, e.Message);
return -1;
}
finally
{
CleanupDirectory(targetDirectory, logger);
}
return 0;
}
private static async Task<string?> GetLatestShaFromPullRequestAsync(IProduct product, HttpClient gitHubClient, int prNumber, ILogger logger, CancellationToken cancellationToken)
{
var requestUri = $"/repos/dotnet/{product.Name.ToLower()}/pulls/{prNumber}";
var response = await gitHubClient.GetAsync(requestUri, cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
var prData = await response.Content.ReadFromJsonAsync<JsonObject>(cancellationToken: cancellationToken).ConfigureAwait(false);
return prData?["head"]?["sha"]?.ToString();
}
else
{
logger.LogError($"Failed to retrieve PR data from GitHub. Status code: {response.StatusCode}");
}
return null;
}
private static async Task PushPRToInternalAsync(IProduct product, int prNumber, string azureBranchName, ILogger logger, string sha, string targetDirectory, CancellationToken cancellationToken)
{
var initCommand = $"init";
await RunGitCommandAsync(initCommand, logger, targetDirectory, cancellationToken).ConfigureAwait(false); ;
var addGithubRemoteCommand = $"remote add {product.Name.ToLower()} {product.RepoHttpBaseUrl}.git";
await RunGitCommandAsync(addGithubRemoteCommand, logger, targetDirectory, cancellationToken).ConfigureAwait(false);
var repoBaseUrl = string.IsNullOrEmpty(product.InternalRepoBaseUrl) ? product.RepoHttpBaseUrl : product.InternalRepoBaseUrl;
var addInternalRemoteCommand = $"remote add internal {repoBaseUrl}";
await RunGitCommandAsync(addInternalRemoteCommand, logger, targetDirectory, cancellationToken).ConfigureAwait(false);
var fetchCommand = $"fetch {product.Name.ToLower()} pull/{prNumber}/head";
await RunGitCommandAsync(fetchCommand, logger, targetDirectory, cancellationToken).ConfigureAwait(false);
var checkoutCommand = $"checkout {sha}";
await RunGitCommandAsync(checkoutCommand, logger, targetDirectory, cancellationToken).ConfigureAwait(false);
var checkoutNewBranchCommand = $"checkout -b {azureBranchName}";
await RunGitCommandAsync(checkoutNewBranchCommand, logger, targetDirectory, cancellationToken).ConfigureAwait(false);
var pushCommand = $"push internal {azureBranchName}";
await RunGitCommandAsync(pushCommand, logger, targetDirectory, cancellationToken).ConfigureAwait(false);
}
private static async Task RunGitCommandAsync(string command, ILogger logger, string workingDirectory, CancellationToken cancellationToken)
{
logger.LogInformation($"Running command: {command}");
var processStartInfo = new ProcessStartInfo
{
FileName = "git",
Arguments = $"{command}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = workingDirectory
};
using var process = new Process
{
StartInfo = processStartInfo
};
var outputBuilder = new StringBuilder();
var errorBuilder = new StringBuilder();
process.OutputDataReceived += (sender, args) => outputBuilder.AppendLine(args.Data);
process.ErrorDataReceived += (sender, args) => errorBuilder.AppendLine(args.Data);
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// Ensure the output and error streams are fully read
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
var output = outputBuilder.ToString();
var error = errorBuilder.ToString();
if (process.ExitCode == 0)
{
logger.LogInformation($"Command succeeded!");
logger.LogInformation(output);
}
else
{
logger.LogError($"Command failed!");
logger.LogError(error);
}
}
private static string CreateDirectory(int prNumber)
{
var targetDirectory = Path.Combine(Path.GetTempPath(), $"pr-{prNumber}");
var counter = 1;
while (Directory.Exists(targetDirectory))
{
targetDirectory = Path.Combine(Path.GetTempPath(), $"pr-{prNumber}-{counter}");
counter++;
}
Directory.CreateDirectory(targetDirectory);
return targetDirectory;
}
private static void CleanupDirectory(string? directory, ILogger logger)
{
if (directory is null)
{
return;
}
try
{
var files = Directory.GetFiles(directory);
var dirs = Directory.GetDirectories(directory);
foreach (string file in files)
{
System.IO.File.SetAttributes(file, FileAttributes.Normal);
System.IO.File.Delete(file);
}
foreach (string dir in dirs)
{
CleanupDirectory(dir, logger);
}
Directory.Delete(directory, false);
}
catch (Exception e)
{
logger.LogError(e, e.Message);
}
}
}

Просмотреть файл

@ -9,6 +9,8 @@ internal class FSharp : IProduct
public string Name => "F#";
public string RepoHttpBaseUrl => "https://github.com/dotnet/fsharp";
public string InternalRepoBaseUrl => "";
public string RepoSshBaseUrl => "git@github.com:dotnet/fsharp.git";
public string GitUserName => "";
public string GitEmail => "";
@ -17,6 +19,7 @@ internal class FSharp : IProduct
public string ComponentName => "Microsoft.FSharp";
public string? PackageName => null;
public string? PackagePropsFileName => null;
public string? DartLabPipelineName => null;
public string? ArtifactsFolderName => null;
public string[] ArtifactsSubFolderNames => Array.Empty<string>();

Просмотреть файл

@ -10,6 +10,7 @@ internal interface IProduct
string RepoHttpBaseUrl { get; }
string RepoSshBaseUrl { get; }
string InternalRepoBaseUrl { get; }
// If commits need to be made against the repo, which user gets the credit
string GitUserName { get; }
@ -19,6 +20,7 @@ internal interface IProduct
string ComponentName { get; }
string? PackageName { get; }
string? PackagePropsFileName { get; }
string? DartLabPipelineName { get; }
string? ArtifactsFolderName { get; }
string[] ArtifactsSubFolderNames { get; }

Просмотреть файл

@ -9,6 +9,8 @@ internal class Razor : IProduct
public string Name => "Razor";
public string RepoHttpBaseUrl => "https://github.com/dotnet/razor";
public string InternalRepoBaseUrl => "";
public string RepoSshBaseUrl => "git@github.com:dotnet/razor.git";
public string GitUserName => "dotnet bot";
public string GitEmail => "dotnet-bot@microsoft.com";
@ -17,10 +19,10 @@ internal class Razor : IProduct
public string ComponentName => "Microsoft.VisualStudio.RazorExtension";
public string? PackageName => null;
public string? PackagePropsFileName => null;
public string? DartLabPipelineName => null;
public string? ArtifactsFolderName => null;
public string[] ArtifactsSubFolderNames => Array.Empty<string>();
public string? GetBuildPipelineName(string buildProjectName)
=> buildProjectName switch
{

Просмотреть файл

@ -9,6 +9,8 @@ internal class Roslyn : IProduct
public string Name => "Roslyn";
public string RepoHttpBaseUrl => "https://github.com/dotnet/roslyn";
public string InternalRepoBaseUrl => "https://dnceng.visualstudio.com/internal/_git/dotnet-roslyn";
public string RepoSshBaseUrl => "git@github.com:dotnet/roslyn.git";
public string GitUserName => "dotnet bot";
public string GitEmail => "dotnet-bot@microsoft.com";
@ -17,10 +19,10 @@ internal class Roslyn : IProduct
public string ComponentName => "Microsoft.CodeAnalysis.LanguageServices";
public string? PackageName => "VS.ExternalAPIs.Roslyn";
public string? PackagePropsFileName => "src/ConfigData/Packages/roslyn.props";
public string? DartLabPipelineName => "Roslyn Integration CI DartLab";
public string? ArtifactsFolderName => "PackageArtifacts";
public string[] ArtifactsSubFolderNames => new[] { "PackageArtifacts/PreRelease", "PackageArtifacts/Release" };
public string? GetBuildPipelineName(string buildProjectName)
=> buildProjectName switch
{

Просмотреть файл

@ -9,6 +9,7 @@ internal class TypeScript : IProduct
public string Name => "TypeScript";
public string RepoHttpBaseUrl => "https://devdiv.visualstudio.com/DevDiv/_git/TypeScript-VS";
public string InternalRepoBaseUrl => "";
public string RepoSshBaseUrl => "devdiv@vs-ssh.visualstudio.com:v3/devdiv/DevDiv/TypeScript-VS";
public string GitUserName => "";
public string GitEmail => "";
@ -16,6 +17,7 @@ internal class TypeScript : IProduct
public string ComponentJsonFileName => @".corext\Configs\components.json";
public string ComponentName => "TypeScript_Tools";
public string? PackageName => "VS.ExternalAPIs.TypeScript.SourceMapReader.dev15";
public string? DartLabPipelineName => null;
public string? ArtifactsFolderName => null;
public string[] ArtifactsSubFolderNames => Array.Empty<string>();

Просмотреть файл

@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the License.txt file in the project root for more information.
using System.Reflection;
using Microsoft.Azure.Pipelines.WebApi;
using Microsoft.Extensions.Logging;
using Microsoft.TeamFoundation.Build.WebApi;
using Microsoft.TeamFoundation.Core.WebApi;
@ -23,6 +25,7 @@ internal sealed class AzDOConnection : IDisposable
public HttpClient NuGetClient { get; }
public FileContainerHttpClient ContainerClient { get; }
public ProjectHttpClient ProjectClient { get; }
public PipelinesHttpClient PipelinesHttpClient { get; }
public AzDOConnection(VssConnection vssConnection, string projectName)
{
@ -37,6 +40,8 @@ internal sealed class AzDOConnection : IDisposable
ContainerClient = Connection.GetClient<FileContainerHttpClient>();
ProjectClient = Connection.GetClient<ProjectHttpClient>();
PipelinesHttpClient = Connection.GetClient<PipelinesHttpClient>();
}
public async Task<List<Build>?> TryGetBuildsAsync(string pipelineName, string? buildNumber = null, ILogger? logger = null, int? maxFetchingVsBuildNumber = null, BuildResult? resultsFilter = null, BuildQueryOrder? buildQueryOrder = null)
@ -64,6 +69,23 @@ internal sealed class AzDOConnection : IDisposable
}
}
public async Task TryRunPipelineAsync(string? pipelineName, Dictionary<string, RepositoryResourceParameters> repositoryParams, RunPipelineParameters runPipelineParams, ILogger logger)
{
try
{
var buildDefinition = (await BuildClient.GetDefinitionsAsync(BuildProjectName, name: pipelineName)).Single();
var repositoryField = runPipelineParams.Resources.GetType().GetField("m_repositories", BindingFlags.NonPublic | BindingFlags.Instance);
repositoryField?.SetValue(runPipelineParams.Resources, repositoryParams);
var run = await PipelinesHttpClient.RunPipelineAsync(runPipelineParams, BuildProjectName, buildDefinition.Id);
logger.LogInformation($"Pipeline running at: {((ReferenceLink)run.Links.Links["web"]).Href}");
}
catch (Exception ex)
{
Console.WriteLine($"Error running pipeline: {ex.Message}");
}
}
public void Dispose()
{
if (!_disposed)

Просмотреть файл

@ -1,4 +1,4 @@
// Licensed to the.NET Foundation under one or more agreements.
// Licensed to the.NET Foundation under one or more agreements.
// The.NET Foundation licenses this file to you under the MIT license.
// See the License.txt file in the project root for more information.
@ -6,11 +6,10 @@ using System.Net.Http.Headers;
using Maestro.Common;
using Maestro.Common.AzureDevOpsTokens;
using Microsoft.RoslynTools.Authentication;
using Microsoft.RoslynTools.Utilities;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;
namespace Microsoft.RoslynTools.PRTagger;
namespace Microsoft.RoslynTools.Utilities;
internal record RemoteConnections : IDisposable
{