зеркало из https://github.com/microsoft/BuildXL.git
Merged PR 813911: Rings and overrides for the build tools installer
Rings and overrides for the build tools installer
This commit is contained in:
Родитель
19f6495e6a
Коммит
5d3a2d4256
|
@ -1,3 +1,4 @@
|
|||
src/bin/*
|
||||
test/bin/*
|
||||
*.nupkg
|
||||
*.nupkg
|
||||
launchSettings.json
|
|
@ -1,12 +1,14 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using BuildToolsInstaller.Config;
|
||||
using BuildToolsInstaller.Logging;
|
||||
using BuildToolsInstaller.Utiltiies;
|
||||
using NuGet.Common;
|
||||
|
||||
namespace BuildToolsInstaller
|
||||
{
|
||||
public record struct BuildToolsInstallerArgs(BuildTool Tool, string ToolsDirectory, string? ConfigFilePath, bool ForceInstallation);
|
||||
public record struct BuildToolsInstallerArgs(BuildTool Tool, string? Ring, string ToolsDirectory, string? ConfigFilePath, bool ForceInstallation);
|
||||
|
||||
/// <summary>
|
||||
/// Entrypoint for the installation logic
|
||||
|
@ -21,19 +23,36 @@ namespace BuildToolsInstaller
|
|||
/// </summary>
|
||||
public static async Task<int> Run(BuildToolsInstallerArgs arguments)
|
||||
{
|
||||
// First, detect if we are running in an ADO build.
|
||||
// This tool is meant to be run on a build, but is also run at image-creation
|
||||
ILogger logger = AdoUtilities.IsAdoBuild ? new AdoConsoleLogger() : new ConsoleLogger();
|
||||
// This tool is primarily run on ADO, but could be also run locally, so we switch on IsEnabled when
|
||||
// we want to do ADO-specific operations.
|
||||
var adoService = AdoService.Instance;
|
||||
ILogger logger = adoService.IsEnabled ? new AdoConsoleLogger() : new ConsoleLogger();
|
||||
|
||||
const string ConfigurationWellKnownUri = "https://bxlscripts.z20.web.core.windows.net/config/DeploymentConfig_V0.json";
|
||||
var deploymentConfiguration = await JsonUtilities.DeserializeFromHttpAsync<DeploymentConfiguration>(new Uri(ConfigurationWellKnownUri), logger, default);
|
||||
if (deploymentConfiguration == null)
|
||||
{
|
||||
// Error should have been logged.
|
||||
return FailureExitCode;
|
||||
}
|
||||
|
||||
IToolInstaller installer = arguments.Tool switch
|
||||
{
|
||||
BuildTool.BuildXL => new BuildXLNugetInstaller(new NugetDownloader(), logger),
|
||||
BuildTool.BuildXL => new BuildXLNugetInstaller(new NugetDownloader(), adoService, logger),
|
||||
|
||||
// Shouldn't happen - the argument comes from a TryParse that should have failed earlier
|
||||
_ => throw new NotImplementedException($"No tool installer for tool {arguments.Tool}"),
|
||||
};
|
||||
|
||||
return await installer.InstallAsync(arguments) ? SuccessExitCode : FailureExitCode;
|
||||
var selectedRing = arguments.Ring ?? installer.DefaultRing;
|
||||
var resolvedVersion = ConfigurationUtilities.ResolveVersion(deploymentConfiguration, selectedRing, arguments.Tool, adoService, logger);
|
||||
if (resolvedVersion == null)
|
||||
{
|
||||
logger.Error("Failed to resolve version to install. Installation has failed.");
|
||||
return FailureExitCode;
|
||||
}
|
||||
|
||||
return await installer.InstallAsync(resolvedVersion, arguments) ? SuccessExitCode : FailureExitCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,6 @@ namespace BuildToolsInstaller.Config
|
|||
/// <summary>
|
||||
/// Latest release version number
|
||||
/// </summary>
|
||||
public required string Release { get; set; }
|
||||
public required string Release { get; init; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ namespace BuildToolsInstaller.Config
|
|||
{
|
||||
/// <summary>
|
||||
/// A specific version to install
|
||||
/// TODO: Support 'Latest' here
|
||||
/// </summary>
|
||||
public string? Version { get; set; }
|
||||
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace BuildToolsInstaller.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Deployment details for a single tool
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For now this just encapsulates a version,
|
||||
/// but leaving it as an object for forwards extensibility
|
||||
/// </remarks>
|
||||
public class ToolDeployment
|
||||
{
|
||||
public required string Version { get; set; }
|
||||
}
|
||||
|
||||
public class RingDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// The identifier for the ring
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional description
|
||||
/// </summary>
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tools available for this ring
|
||||
/// </summary>
|
||||
public required IReadOnlyDictionary<BuildTool, ToolDeployment> Tools { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An override to the default version of a tool that would be resolved for a build based on its selected ring
|
||||
/// </summary>
|
||||
public class DeploymentOverride
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional comment describing the override
|
||||
/// </summary>
|
||||
public string? Comment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The exception is applied to builds running under the repository with this name
|
||||
/// </summary>
|
||||
public required string Repository { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// If this is defined, the exception applies only to the specified pipelines
|
||||
/// </summary>
|
||||
public IReadOnlyList<int>? PipelineIds { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The overrides for this exception
|
||||
/// </summary>
|
||||
public required IReadOnlyDictionary<BuildTool, ToolDeployment> Tools { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The main configuration object
|
||||
/// </summary>
|
||||
public class DeploymentConfiguration
|
||||
{
|
||||
/// <nodoc />
|
||||
public required IReadOnlyList<RingDefinition> Rings { get; init; }
|
||||
|
||||
/// <nodoc />
|
||||
public IReadOnlyList<DeploymentOverride>? Overrides { get; init; }
|
||||
}
|
||||
}
|
|
@ -19,6 +19,10 @@ namespace BuildToolsInstaller
|
|||
/// </summary>
|
||||
public class BuildXLNugetInstaller : IToolInstaller
|
||||
{
|
||||
// Default ring for BuildXL installation
|
||||
public string DefaultRing => "GeneralPublic";
|
||||
|
||||
private readonly IAdoService m_adoService;
|
||||
private readonly INugetDownloader m_downloader;
|
||||
private readonly ILogger m_logger;
|
||||
private BuildXLNugetInstallerConfig? m_config;
|
||||
|
@ -30,14 +34,15 @@ namespace BuildToolsInstaller
|
|||
// Use only after calling TryInitializeConfig()
|
||||
private BuildXLNugetInstallerConfig Config => m_config!;
|
||||
|
||||
public BuildXLNugetInstaller(INugetDownloader downloader, ILogger logger)
|
||||
public BuildXLNugetInstaller(INugetDownloader downloader, IAdoService adoService, ILogger logger)
|
||||
{
|
||||
m_adoService = adoService;
|
||||
m_downloader = downloader;
|
||||
m_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> InstallAsync(BuildToolsInstallerArgs args)
|
||||
public async Task<bool> InstallAsync(string selectedVersion, BuildToolsInstallerArgs args)
|
||||
{
|
||||
if (!await TryInitializeConfigAsync(args))
|
||||
{
|
||||
|
@ -46,10 +51,11 @@ namespace BuildToolsInstaller
|
|||
|
||||
try
|
||||
{
|
||||
var version = await TryResolveVersionAsync();
|
||||
// Version override
|
||||
var version = await TryResolveVersionAsync(selectedVersion);
|
||||
if (version == null)
|
||||
{
|
||||
m_logger.Error("BuildLXNugetInstaller: failed to resolve version to install.");
|
||||
// Error should have been logged
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -85,7 +91,7 @@ namespace BuildToolsInstaller
|
|||
return false;
|
||||
}
|
||||
|
||||
var feed = Config.FeedOverride ?? InferSourceRepository();
|
||||
var feed = Config.FeedOverride ?? InferSourceRepository(m_adoService);
|
||||
|
||||
var repository = CreateSourceRepository(feed);
|
||||
if (await m_downloader.TryDownloadNugetToDiskAsync(repository, PackageName, nugetVersion, downloadLocation, m_logger))
|
||||
|
@ -120,14 +126,14 @@ namespace BuildToolsInstaller
|
|||
/// <summary>
|
||||
/// Construct the implicit source repository for installers, a well-known feed that should be installed in the organization
|
||||
/// </summary>
|
||||
private static string InferSourceRepository()
|
||||
private static string InferSourceRepository(IAdoService adoService)
|
||||
{
|
||||
if (!AdoUtilities.IsAdoBuild)
|
||||
if (!adoService.IsEnabled)
|
||||
{
|
||||
throw new InvalidOperationException("Automatic source repository inference is only supported when running on an ADO Build");
|
||||
}
|
||||
|
||||
if (!AdoUtilities.TryGetOrganizationName(out var adoOrganizationName))
|
||||
if (!adoService.TryGetOrganizationName(out var adoOrganizationName))
|
||||
{
|
||||
throw new InvalidOperationException("Could not retrieve organization name");
|
||||
}
|
||||
|
@ -152,7 +158,10 @@ namespace BuildToolsInstaller
|
|||
|
||||
private void SetLocationVariable(string engineLocation)
|
||||
{
|
||||
AdoUtilities.SetVariable("ONEES_BUILDXL_LOCATION", engineLocation, isReadOnly: true);
|
||||
if (m_adoService.IsEnabled)
|
||||
{
|
||||
m_adoService.SetVariable("ONEES_BUILDXL_LOCATION", engineLocation, isReadOnly: true);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> TryInitializeConfigAsync(BuildToolsInstallerArgs args)
|
||||
|
@ -163,14 +172,14 @@ namespace BuildToolsInstaller
|
|||
return true;
|
||||
}
|
||||
|
||||
m_config = await JsonDeserializer.DeserializeAsync<BuildXLNugetInstallerConfig>(args.ConfigFilePath, m_logger, CancellationToken.None);
|
||||
m_config = await JsonUtilities.DeserializeAsync<BuildXLNugetInstallerConfig>(args.ConfigFilePath, m_logger, serializerOptions: new () { PropertyNameCaseInsensitive = true });
|
||||
if (m_config == null)
|
||||
{
|
||||
m_logger.Error("Could not parse the BuildXL installer configuration. Installation will fail.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_config.DistributedRole != null && !AdoUtilities.IsAdoBuild)
|
||||
if (m_config.DistributedRole != null && !m_adoService.IsEnabled)
|
||||
{
|
||||
m_logger.Error("Distributed mode can only be enabld in ADO Builds. Installation will fail.");
|
||||
return false;
|
||||
|
@ -187,7 +196,7 @@ namespace BuildToolsInstaller
|
|||
return Environment.GetEnvironmentVariable("BUILDTOOLSDOWNLOADER_NUGET_PAT") ?? Environment.GetEnvironmentVariable("SYSTEM_ACCESSTOKEN") ?? "";
|
||||
}
|
||||
|
||||
private async Task<string?> TryResolveVersionAsync()
|
||||
private async Task<string?> TryResolveVersionAsync(string selectedVersion)
|
||||
{
|
||||
var resolvedVersionProperty = "BuildXLResolvedVersion_" + (Config.InvocationKey ?? "default");
|
||||
string? resolvedVersion = null;
|
||||
|
@ -216,25 +225,16 @@ namespace BuildToolsInstaller
|
|||
}
|
||||
else
|
||||
{
|
||||
// Orchestrator (or default) mode -> resolve version from global configuration an publish it
|
||||
const string ConfigurationWellKnownUri = "https://bxlscripts.z20.web.core.windows.net/config/buildxl/BuildXLConfig_V0.json";
|
||||
var jsonUri = new Uri(Config.Internal_GlobalConfigOverride ?? ConfigurationWellKnownUri);
|
||||
var config = await JsonDeserializer.DeserializeFromHttpAsync<BuildXLGlobalConfig_V0>(jsonUri, m_logger, default);
|
||||
if (config == null)
|
||||
{
|
||||
// Error should have been logged.
|
||||
return null;
|
||||
}
|
||||
|
||||
resolvedVersion = config.Release;
|
||||
// Orchestrator (or default) mode
|
||||
resolvedVersion = selectedVersion;
|
||||
}
|
||||
|
||||
if (Config.DistributedRole== DistributedRole.Orchestrator)
|
||||
if (Config.DistributedRole == DistributedRole.Orchestrator)
|
||||
{
|
||||
// We resolved a version - we should push it to the properties for the workers to consume.
|
||||
// If the version is null it means we encountered some error above, so push the empty string
|
||||
// (the workers must be signalled that there was an error somehow).
|
||||
await AdoUtilities.SetBuildPropertyAsync(resolvedVersionProperty, resolvedVersion ?? string.Empty);
|
||||
await m_adoService.SetBuildPropertyAsync(resolvedVersionProperty, resolvedVersion ?? string.Empty);
|
||||
}
|
||||
|
||||
return resolvedVersion;
|
||||
|
@ -247,7 +247,7 @@ namespace BuildToolsInstaller
|
|||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var maybeProperty = await AdoUtilities.GetBuildPropertyAsync(propertyKey);
|
||||
var maybeProperty = await m_adoService.GetBuildPropertyAsync(propertyKey);
|
||||
if (maybeProperty != null)
|
||||
{
|
||||
// Orchestrator pushes an empty string on error
|
||||
|
|
|
@ -11,6 +11,11 @@ namespace BuildToolsInstaller
|
|||
/// <summary>
|
||||
/// Install to the given directory
|
||||
/// </summary>
|
||||
public Task<bool> InstallAsync(BuildToolsInstallerArgs args);
|
||||
public Task<bool> InstallAsync(string selectedVersion, BuildToolsInstallerArgs args);
|
||||
|
||||
/// <summary>
|
||||
/// The name of the default ring for this tool
|
||||
/// </summary>
|
||||
public string DefaultRing { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,10 @@ namespace BuildToolsInstaller
|
|||
description: "The tool to install.")
|
||||
{ IsRequired = true };
|
||||
|
||||
var ringOption = new Option<string?>(
|
||||
name: "--ring",
|
||||
description: "Selects a deployment ring for the tool");
|
||||
|
||||
var toolsDirectoryOption = new Option<string?>(
|
||||
name: "--toolsDirectory",
|
||||
description: "The location where packages should be downloaded. Defaults to AGENT_TOOLSDIRECTORY if defined, or the working directory if not");
|
||||
|
@ -54,23 +58,26 @@ namespace BuildToolsInstaller
|
|||
|
||||
var rootCommand = new RootCommand("Build tools installer");
|
||||
rootCommand.AddOption(toolOption);
|
||||
rootCommand.AddOption(ringOption);
|
||||
rootCommand.AddOption(toolsDirectoryOption);
|
||||
rootCommand.AddOption(configOption);
|
||||
rootCommand.AddOption(forceOption);
|
||||
|
||||
int returnCode = ProgramNotRunExitCode; // Make the compiler happy, we should assign every time
|
||||
rootCommand.SetHandler(async (tool, toolsDirectory, configFile, forceInstallation) =>
|
||||
rootCommand.SetHandler(async (tool, ring, toolsDirectory, configFile, forceInstallation) =>
|
||||
{
|
||||
toolsDirectory ??= AdoUtilities.ToolsDirectory ?? ".";
|
||||
toolsDirectory ??= AdoService.Instance.IsEnabled ? AdoService.Instance.ToolsDirectory : ".";
|
||||
returnCode = await BuildToolsInstaller.Run(new BuildToolsInstallerArgs()
|
||||
{
|
||||
Tool = tool,
|
||||
Ring = ring,
|
||||
ToolsDirectory = toolsDirectory,
|
||||
ConfigFilePath = configFile,
|
||||
ForceInstallation = forceInstallation
|
||||
});
|
||||
},
|
||||
toolOption,
|
||||
ringOption,
|
||||
toolsDirectoryOption,
|
||||
configOption,
|
||||
forceOption
|
||||
|
|
|
@ -1,56 +1,89 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.TeamFoundation.Build.WebApi;
|
||||
using Microsoft.VisualStudio.Services.Common;
|
||||
using Microsoft.VisualStudio.Services.WebApi;
|
||||
using NuGet.Protocol.Plugins;
|
||||
|
||||
namespace BuildToolsInstaller.Utiltiies
|
||||
{
|
||||
internal sealed partial class AdoUtilities
|
||||
internal sealed partial class AdoService : IAdoService
|
||||
{
|
||||
// Make this class a singleton
|
||||
private AdoService() { }
|
||||
|
||||
public static AdoService Instance { get; } = s_instance ??= new();
|
||||
// Keep as lazily initialized for the sake of testing outside of ADO (where we need to modify the environment first)
|
||||
private static AdoService? s_instance;
|
||||
|
||||
/// <summary>
|
||||
/// True if the process is running in an ADO build.
|
||||
/// The other methods and properties in this class are meaningful if this is true.
|
||||
/// </summary>
|
||||
public static bool IsAdoBuild => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TF_BUILD"));
|
||||
public bool IsEnabled => m_isAdoBuild;
|
||||
private readonly bool m_isAdoBuild = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TF_BUILD"));
|
||||
|
||||
[GeneratedRegex(@"^(?:https://(?<oldSchoolAccountName>[a-zA-Z0-9-]+)\.(?:vsrm\.)?visualstudio\.com/|https://(?:vsrm\.)?dev\.azure\.com/(?<newSchoolAccountName>[a-zA-Z0-9-]+)/)$", RegexOptions.CultureInvariant)]
|
||||
private static partial Regex CollectionUriRegex();
|
||||
|
||||
/// <nodoc />
|
||||
public static string CollectionUri => Environment.GetEnvironmentVariable("SYSTEM_COLLECTIONURI")!;
|
||||
/// <nodoc />
|
||||
public static string ToolsDirectory => Environment.GetEnvironmentVariable("AGENT_TOOLSDIRECTORY")!;
|
||||
|
||||
/// <nodoc />
|
||||
public static string AccessToken => Environment.GetEnvironmentVariable("SYSTEM_ACCESSTOKEN")!;
|
||||
|
||||
/// <nodoc />
|
||||
private static string ServerUri => Environment.GetEnvironmentVariable("SYSTEM_TEAMFOUNDATIONSERVERURI")!;
|
||||
|
||||
/// <nodoc />
|
||||
private static string ProjectId => Environment.GetEnvironmentVariable("SYSTEM_TEAMPROJECTID")!;
|
||||
|
||||
/// <nodoc />
|
||||
public static string BuildId => Environment.GetEnvironmentVariable("BUILD_BUILDID")!;
|
||||
|
||||
private static BuildHttpClient BuildClient => s_httpClient ??= new BuildHttpClient(new Uri(ServerUri), new VssBasicCredential(string.Empty, AccessToken));
|
||||
private static BuildHttpClient? s_httpClient;
|
||||
|
||||
public static async Task<string?> GetBuildPropertyAsync(string key)
|
||||
private void EnsureAdo()
|
||||
{
|
||||
if (!IsEnabled)
|
||||
{
|
||||
throw new InvalidOperationException($"This operation in {nameof(AdoService)} is only available in an ADO build");
|
||||
}
|
||||
}
|
||||
|
||||
private T EnsuringAdo<T>(T? ret)
|
||||
{
|
||||
EnsureAdo();
|
||||
return ret!;
|
||||
}
|
||||
|
||||
#region Predefined variables - see https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables
|
||||
/// <nodoc />
|
||||
public string CollectionUri => EnsuringAdo(Environment.GetEnvironmentVariable("SYSTEM_COLLECTIONURI"));
|
||||
|
||||
/// <nodoc />
|
||||
public string ToolsDirectory => EnsuringAdo(Environment.GetEnvironmentVariable("AGENT_TOOLSDIRECTORY"));
|
||||
|
||||
/// <nodoc />
|
||||
public string AccessToken => EnsuringAdo(Environment.GetEnvironmentVariable("SYSTEM_ACCESSTOKEN"));
|
||||
|
||||
/// <nodoc />
|
||||
private string ServerUri => EnsuringAdo(Environment.GetEnvironmentVariable("SYSTEM_TEAMFOUNDATIONSERVERURI"));
|
||||
|
||||
/// <nodoc />
|
||||
private string ProjectId => EnsuringAdo(Environment.GetEnvironmentVariable("SYSTEM_TEAMPROJECTID"));
|
||||
|
||||
/// <nodoc />
|
||||
public string BuildId => EnsuringAdo(Environment.GetEnvironmentVariable("BUILD_BUILDID"));
|
||||
|
||||
/// <nodoc />
|
||||
public string RepositoryName => Environment.GetEnvironmentVariable("BUILD_REPOSITORY_NAME")!;
|
||||
|
||||
/// <nodoc />
|
||||
public int PipelineId => int.Parse(Environment.GetEnvironmentVariable("SYSTEM_DEFINITIONID")!);
|
||||
#endregion
|
||||
|
||||
private BuildHttpClient BuildClient => m_httpClient ??= new BuildHttpClient(new Uri(ServerUri), new VssBasicCredential(string.Empty, AccessToken));
|
||||
private BuildHttpClient? m_httpClient;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string?> GetBuildPropertyAsync(string key)
|
||||
{
|
||||
EnsureAdo();
|
||||
var props = await IdempotentWithRetry(() => BuildClient.GetBuildPropertiesAsync(ProjectId, int.Parse(BuildId)));
|
||||
return props.ContainsKey(key) ? props.GetValue(key, string.Empty) : null;
|
||||
}
|
||||
|
||||
public static async Task SetBuildPropertyAsync(string key, string value)
|
||||
/// <inheritdoc />
|
||||
public async Task SetBuildPropertyAsync(string key, string value)
|
||||
{
|
||||
|
||||
EnsureAdo();
|
||||
// UpdateBuildProperties is ultimately an HTTP PATCH: the new properties specified will be added to the existing ones
|
||||
// in an atomic fashion. So we don't have to worry about multiple builds concurrently calling UpdateBuildPropertiesAsync
|
||||
// as long as the keys don't clash.
|
||||
|
@ -82,8 +115,9 @@ namespace BuildToolsInstaller.Utiltiies
|
|||
/// <summary>
|
||||
/// Get the organization name from environment data in the agent
|
||||
/// </summary>
|
||||
public static bool TryGetOrganizationName([NotNullWhen(true)] out string? organizationName)
|
||||
public bool TryGetOrganizationName([NotNullWhen(true)] out string? organizationName)
|
||||
{
|
||||
EnsureAdo();
|
||||
organizationName = null;
|
||||
string collectionUri = CollectionUri;
|
||||
if (collectionUri == null)
|
||||
|
@ -105,12 +139,11 @@ namespace BuildToolsInstaller.Utiltiies
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a variable that will be visible by subsequent tasks in the running job
|
||||
/// </summary>
|
||||
public static void SetVariable(string variableName, string value, bool isReadOnly = true)
|
||||
/// <inheritdoc />
|
||||
public void SetVariable(string variableName, string value, bool isReadOnly = true)
|
||||
{
|
||||
if (!IsAdoBuild)
|
||||
EnsureAdo();
|
||||
if (!IsEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using BuildToolsInstaller.Config;
|
||||
|
||||
namespace BuildToolsInstaller.Utiltiies
|
||||
{
|
||||
internal class ConfigurationUtilities
|
||||
{
|
||||
public static string? ResolveVersion(DeploymentConfiguration deploymentConfiguration, string ring, BuildTool tool, IAdoService adoService, ILogger logger)
|
||||
{
|
||||
// 1. Check overrides
|
||||
if (TryGetFromOverride(deploymentConfiguration, tool, adoService, out var resolvedVersion, logger))
|
||||
{
|
||||
return resolvedVersion;
|
||||
}
|
||||
|
||||
// 2. Resolve from ring
|
||||
var selectedRing = deploymentConfiguration.Rings.FirstOrDefault(r => r.Name == ring);
|
||||
if (selectedRing == null)
|
||||
{
|
||||
logger.Error($"Could not find configuration for ring {ring}. Available rings are: [{string.Join(", ", deploymentConfiguration.Rings.Select(r => r.Name))}]");
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!selectedRing.Tools.TryGetValue(tool, out var resolved))
|
||||
{
|
||||
logger.Error($"Could not find configuration for tool {tool} in ring {ring}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return resolved.Version;
|
||||
}
|
||||
|
||||
private static bool TryGetFromOverride(DeploymentConfiguration deploymentConfiguration, BuildTool tool, IAdoService adoService, [NotNullWhen(true)] out string? resolvedVersion, ILogger logger)
|
||||
{
|
||||
resolvedVersion = null;
|
||||
if (!adoService.IsEnabled || deploymentConfiguration.Overrides is null || deploymentConfiguration.Overrides.Count == 0)
|
||||
{
|
||||
// Overrides can only be applied when running an ADO build
|
||||
return false;
|
||||
}
|
||||
|
||||
var repository = adoService.RepositoryName;
|
||||
var pipelineId = adoService.PipelineId;
|
||||
|
||||
foreach (var exception in deploymentConfiguration.Overrides)
|
||||
{
|
||||
if (string.Equals(exception.Repository, repository, StringComparison.OrdinalIgnoreCase)
|
||||
&& (exception.PipelineIds == null || exception.PipelineIds.Contains(pipelineId))
|
||||
&& exception.Tools.TryGetValue(tool, out var toolDeployment))
|
||||
{
|
||||
resolvedVersion = toolDeployment.Version;
|
||||
var details = string.IsNullOrEmpty(exception.Comment) ? string.Empty : $" Details: {exception.Comment}";
|
||||
logger.Info($"Selecting version {resolvedVersion} for tool {tool} from a global configuration override.{details}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// No matches
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace BuildToolsInstaller.Utiltiies
|
||||
{
|
||||
/// <summary>
|
||||
/// Component that interacts with Azure DevOps, using the REST API or the environment
|
||||
/// Abstracted as an interface for ease of testing
|
||||
/// </summary>
|
||||
public interface IAdoService
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether ADO interaction is enabled
|
||||
/// This is false when not running on an agent, and other methods may throw in this case
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; }
|
||||
|
||||
/// <nodoc />
|
||||
public string CollectionUri { get; }
|
||||
|
||||
/// <nodoc />
|
||||
public string ToolsDirectory { get; }
|
||||
|
||||
/// <nodoc />
|
||||
public string AccessToken { get; }
|
||||
|
||||
/// <nodoc />
|
||||
public string BuildId { get; }
|
||||
|
||||
/// <nodoc />
|
||||
public string RepositoryName { get; }
|
||||
|
||||
/// <nodoc />
|
||||
public int PipelineId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets a build property with the specified value using the ADO REST API
|
||||
/// </summary>
|
||||
public Task SetBuildPropertyAsync(string key, string value);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of a build property using the REST API, or null if such property is not defined
|
||||
/// </summary>
|
||||
public Task<string?> GetBuildPropertyAsync(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Get the organization name from environment data in the agent
|
||||
/// </summary>
|
||||
public bool TryGetOrganizationName([NotNullWhen(true)] out string? organizationName);
|
||||
|
||||
/// <summary>
|
||||
/// Set a variable that will be visible by subsequent tasks in the running job
|
||||
/// </summary>
|
||||
public void SetVariable(string variableName, string value, bool isReadOnly = true);
|
||||
}
|
||||
}
|
|
@ -10,16 +10,20 @@ namespace BuildToolsInstaller.Utiltiies
|
|||
/// <summary>
|
||||
/// Deserializing utilities
|
||||
/// </summary>
|
||||
public static class JsonDeserializer
|
||||
public static class JsonUtilities
|
||||
{
|
||||
private static readonly HttpClient s_httpClient = new HttpClient();
|
||||
private const int MaxRetries = 3;
|
||||
private static readonly TimeSpan s_delayBetweenRetries = TimeSpan.FromSeconds(2);
|
||||
internal static readonly JsonSerializerOptions DefaultSerializerOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize from a file stored in an Azure Storage blob, logging an error and returning null if the operation fails
|
||||
/// </summary>
|
||||
public static async Task<T?> DeserializeFromBlobAsync<T>(Uri blobUri, ILogger logger, CancellationToken token)
|
||||
public static async Task<T?> DeserializeFromBlobAsync<T>(Uri blobUri, ILogger logger, JsonSerializerOptions? serializerOptions = null, CancellationToken token = default)
|
||||
{
|
||||
// The storage account should have been configured with anonymous read access to the config blob,
|
||||
// so no need to provide any credentials.
|
||||
|
@ -39,7 +43,7 @@ namespace BuildToolsInstaller.Utiltiies
|
|||
return default;
|
||||
}
|
||||
|
||||
return await DeserializeAsync<T>(downloadPath, logger, token);
|
||||
return await DeserializeAsync<T>(downloadPath, logger, serializerOptions, token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -57,7 +61,7 @@ namespace BuildToolsInstaller.Utiltiies
|
|||
/// <summary>
|
||||
/// Deserializes a JSON file pointed by <paramref name="uri"/>. The request is retried upon HTTP failures.
|
||||
/// </summary>
|
||||
public static async Task<T?> DeserializeFromHttpAsync<T>(Uri uri, ILogger logger, CancellationToken token)
|
||||
public static async Task<T?> DeserializeFromHttpAsync<T>(Uri uri, ILogger logger, JsonSerializerOptions? serializerOptions = null, CancellationToken token = default)
|
||||
{
|
||||
int retryCount = 0;
|
||||
|
||||
|
@ -70,7 +74,7 @@ namespace BuildToolsInstaller.Utiltiies
|
|||
response.EnsureSuccessStatusCode();
|
||||
await using (Stream responseStream = await response.Content.ReadAsStreamAsync(token))
|
||||
{
|
||||
T? result = await JsonSerializer.DeserializeAsync<T>(responseStream, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }, token);
|
||||
T? result = await JsonSerializer.DeserializeAsync<T>(responseStream, serializerOptions ?? DefaultSerializerOptions, token);
|
||||
logger.Info($"Successfully deserialized JSON from {uri}.");
|
||||
return result;
|
||||
}
|
||||
|
@ -102,13 +106,13 @@ namespace BuildToolsInstaller.Utiltiies
|
|||
/// <summary>
|
||||
/// Deserialize JSON from a file, logging an error and returning null if the operation fails
|
||||
/// </summary>
|
||||
public static async Task<T?> DeserializeAsync<T>(string filePath, ILogger logger, CancellationToken token)
|
||||
public static async Task<T?> DeserializeAsync<T>(string filePath, ILogger logger, JsonSerializerOptions? serializerOptions = null, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (FileStream openStream = File.OpenRead(filePath))
|
||||
{
|
||||
return await JsonSerializer.DeserializeAsync<T>(openStream, cancellationToken: token);
|
||||
return await JsonSerializer.DeserializeAsync<T>(openStream, serializerOptions ?? DefaultSerializerOptions, cancellationToken: token);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
|
@ -1,12 +1,7 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BuildToolsInstaller.Utiltiies;
|
||||
using Xunit;
|
||||
|
||||
|
@ -26,9 +21,10 @@ namespace BuildToolsInstaller.Tests
|
|||
modifyEnvironment.Set("SYSTEM_COLLECTIONURI", "https://mseng.visualstudio.com/");
|
||||
modifyEnvironment.Set("TF_BUILD", "True");
|
||||
|
||||
Assert.True(AdoUtilities.IsAdoBuild);
|
||||
Assert.Equal(toolsDirectory, AdoUtilities.ToolsDirectory);
|
||||
Assert.True(AdoUtilities.TryGetOrganizationName(out var orgName));
|
||||
var adoService = AdoService.Instance;
|
||||
Assert.True(adoService.IsEnabled);
|
||||
Assert.Equal(toolsDirectory, adoService.ToolsDirectory);
|
||||
Assert.True(adoService.TryGetOrganizationName(out var orgName));
|
||||
Assert.Equal("mseng", orgName);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,24 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BuildToolsInstaller.Utiltiies;
|
||||
using Xunit;
|
||||
|
||||
namespace BuildToolsInstaller.Tests
|
||||
{
|
||||
public class BuildXLInstallerTests
|
||||
{
|
||||
const string DefaultRing = "GeneralPublic";
|
||||
const string DefaultVersion = "0.1.0-20252610.1";
|
||||
|
||||
[Fact]
|
||||
public async Task AdoEnvironmentPickedUpByDefault()
|
||||
{
|
||||
// We may modify the environment during the test, this will restore it on dispose
|
||||
using var modifyEnvironment = new TemporaryTestEnvironment();
|
||||
|
||||
// If we're not in ADO, this will simulate that we are
|
||||
// If we're not in ADO, this will simulate that we are
|
||||
var toolsDirectory = Path.Combine(Path.GetTempPath(), "Test");
|
||||
modifyEnvironment.Set("AGENT_TOOLSDIRECTORY", toolsDirectory);
|
||||
modifyEnvironment.Set("SYSTEM_COLLECTIONURI", "https://mseng.visualstudio.com/");
|
||||
modifyEnvironment.Set("TF_BUILD", "True");
|
||||
var mockAdoService = new MockAdoService()
|
||||
{
|
||||
ToolsDirectory = toolsDirectory
|
||||
};
|
||||
|
||||
var configTempPath = Path.GetTempFileName();
|
||||
File.WriteAllText(configTempPath, """
|
||||
|
@ -33,16 +31,16 @@ namespace BuildToolsInstaller.Tests
|
|||
var mockDownloader = new MockNugetDownloader();
|
||||
var log = new TestLogger();
|
||||
|
||||
var args = new BuildToolsInstallerArgs(BuildTool.BuildXL, AdoUtilities.ToolsDirectory, configTempPath, false);
|
||||
var buildXLInstaller = new BuildXLNugetInstaller(mockDownloader, log);
|
||||
var result = await buildXLInstaller.InstallAsync(args);
|
||||
var args = new BuildToolsInstallerArgs(BuildTool.BuildXL, "GeneralPublic", mockAdoService.ToolsDirectory, configTempPath, false);
|
||||
var buildXLInstaller = new BuildXLNugetInstaller(mockDownloader, mockAdoService, log);
|
||||
var result = await buildXLInstaller.InstallAsync(DefaultVersion, args);
|
||||
Assert.True(result, log.FullLog);
|
||||
|
||||
Assert.Single(mockDownloader.Downloads);
|
||||
|
||||
var download = mockDownloader.Downloads[0];
|
||||
// Default feed is within the org
|
||||
Assert.StartsWith("https://pkgs.dev.azure.com/mseng/_packaging", download.Repository);
|
||||
Assert.StartsWith($"https://pkgs.dev.azure.com/{MockAdoService.OrgName}/_packaging", download.Repository);
|
||||
Assert.Equal("0.1.0-20241026.1", download.Version);
|
||||
}
|
||||
finally
|
||||
|
@ -59,6 +57,12 @@ namespace BuildToolsInstaller.Tests
|
|||
[Fact]
|
||||
public async Task InstallWithCustomConfig()
|
||||
{
|
||||
var downloadDirectory = Path.Join(Path.GetTempPath(), $"Test_{nameof(InstallWithCustomConfig)}");
|
||||
if (Directory.Exists(downloadDirectory))
|
||||
{
|
||||
Directory.Delete(downloadDirectory, true);
|
||||
}
|
||||
|
||||
var configTempPath = Path.GetTempFileName();
|
||||
File.WriteAllText(configTempPath, """
|
||||
{
|
||||
|
@ -71,10 +75,15 @@ namespace BuildToolsInstaller.Tests
|
|||
var mockDownloader = new MockNugetDownloader();
|
||||
var log = new TestLogger();
|
||||
|
||||
var downloadDirectory = Path.GetTempPath();
|
||||
var args = new BuildToolsInstallerArgs(BuildTool.BuildXL, downloadDirectory, configTempPath, false);
|
||||
var buildXLInstaller = new BuildXLNugetInstaller(mockDownloader, log);
|
||||
var result = await buildXLInstaller.InstallAsync(args);
|
||||
var mockAdoService = new MockAdoService()
|
||||
{
|
||||
ToolsDirectory = downloadDirectory
|
||||
};
|
||||
|
||||
var args = new BuildToolsInstallerArgs(BuildTool.BuildXL, DefaultRing, downloadDirectory, configTempPath, false);
|
||||
|
||||
var buildXLInstaller = new BuildXLNugetInstaller(mockDownloader, mockAdoService, log);
|
||||
var result = await buildXLInstaller.InstallAsync(DefaultVersion, args);
|
||||
Assert.True(result, log.FullLog);
|
||||
|
||||
Assert.Single(mockDownloader.Downloads);
|
||||
|
@ -112,9 +121,10 @@ namespace BuildToolsInstaller.Tests
|
|||
var mockDownloader = new MockNugetDownloader();
|
||||
var log = new TestLogger();
|
||||
|
||||
var args = new BuildToolsInstallerArgs(BuildTool.BuildXL, toolsDirectory, configTempPath, force);
|
||||
var buildXLInstaller = new BuildXLNugetInstaller(mockDownloader, log);
|
||||
var result = await buildXLInstaller.InstallAsync(args);
|
||||
var args = new BuildToolsInstallerArgs(BuildTool.BuildXL, DefaultRing, toolsDirectory, configTempPath, force);
|
||||
var mockAdoService = new MockAdoService() { ToolsDirectory = toolsDirectory };
|
||||
var buildXLInstaller = new BuildXLNugetInstaller(mockDownloader, mockAdoService, log);
|
||||
var result = await buildXLInstaller.InstallAsync(DefaultVersion, args);
|
||||
Assert.True(result, log.FullLog);
|
||||
|
||||
if (force)
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using BuildToolsInstaller.Config;
|
||||
using BuildToolsInstaller.Utiltiies;
|
||||
using Xunit;
|
||||
|
||||
namespace BuildToolsInstaller.Tests
|
||||
{
|
||||
public class DeploymentConfigurationTests
|
||||
{
|
||||
private const string TestConfiguration = @"
|
||||
{
|
||||
""rings"": [
|
||||
{
|
||||
""name"": ""Dogfood"",
|
||||
""description"": ""Dogfood ring"",
|
||||
""tools"": {
|
||||
""BuildXL"": { ""version"": ""0.1.0-20250101.1"" }
|
||||
}
|
||||
},
|
||||
{
|
||||
""name"": ""GeneralPublic"",
|
||||
""tools"": {
|
||||
""BuildXL"": { ""version"": ""0.1.0-20241025.4"" }
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
""overrides"":
|
||||
[
|
||||
{
|
||||
""comment"": ""description of the pin"",
|
||||
""repository"": ""1JS"",
|
||||
""tools"": {
|
||||
""BuildXL"": { ""version"": ""0.1.0-20240801.1"" }
|
||||
}
|
||||
},
|
||||
{
|
||||
""repository"": ""BuildXL.Internal"",
|
||||
""pipelineIds"": [10101, 1010],
|
||||
""tools"": {
|
||||
""BuildXL"": { ""version"": ""0.1.0-20240801.1"" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
";
|
||||
|
||||
[Fact]
|
||||
public void DeserializationTest()
|
||||
{
|
||||
var deserialized = JsonSerializer.Deserialize<DeploymentConfiguration>(TestConfiguration, JsonUtilities.DefaultSerializerOptions);
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.NotNull(deserialized.Overrides);
|
||||
Assert.NotNull(deserialized.Rings);
|
||||
Assert.Equal(2, deserialized.Overrides.Count);
|
||||
Assert.Equal(2, deserialized.Rings.Count);
|
||||
Assert.Equal("Dogfood", deserialized.Rings[0].Name);
|
||||
Assert.Equal("Dogfood ring", deserialized.Rings[0].Description);
|
||||
Assert.Contains(BuildTool.BuildXL, deserialized.Rings[0].Tools.Keys);
|
||||
Assert.Equal("0.1.0-20250101.1", deserialized.Rings[0].Tools[BuildTool.BuildXL].Version);
|
||||
Assert.Equal("description of the pin", deserialized.Overrides[0].Comment);
|
||||
Assert.Equal("1JS", deserialized.Overrides[0].Repository);
|
||||
Assert.Contains(BuildTool.BuildXL, deserialized.Overrides[0].Tools.Keys);
|
||||
Assert.Equal("0.1.0-20240801.1", deserialized.Overrides[0].Tools[BuildTool.BuildXL].Version);
|
||||
Assert.NotNull(deserialized.Overrides[1].PipelineIds);
|
||||
Assert.Equal(2, deserialized.Overrides[1].PipelineIds!.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolutionByRingTest()
|
||||
{
|
||||
var config = new DeploymentConfiguration()
|
||||
{
|
||||
Rings = [
|
||||
new RingDefinition() {
|
||||
Name = "A",
|
||||
Tools = new ReadOnlyDictionary<BuildTool, ToolDeployment>(new Dictionary<BuildTool, ToolDeployment> () {
|
||||
{ BuildTool.BuildXL, new ToolDeployment() { Version = "VersionA" } }
|
||||
})
|
||||
},
|
||||
new RingDefinition() {
|
||||
Name = "B",
|
||||
Tools = new ReadOnlyDictionary<BuildTool, ToolDeployment>(new Dictionary<BuildTool, ToolDeployment> () {
|
||||
{ BuildTool.BuildXL, new ToolDeployment() { Version = "VersionB" } }
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var mockAdoService = new MockAdoService() { ToolsDirectory = Path.GetTempPath() };
|
||||
Assert.Equal("VersionA", ConfigurationUtilities.ResolveVersion(config, "A", BuildTool.BuildXL, mockAdoService, new TestLogger()));
|
||||
Assert.Equal("VersionB", ConfigurationUtilities.ResolveVersion(config, "B", BuildTool.BuildXL, mockAdoService, new TestLogger()));
|
||||
Assert.Null(ConfigurationUtilities.ResolveVersion(config, "C", BuildTool.BuildXL, mockAdoService, new TestLogger()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, true)]
|
||||
public void Exceptions(bool pinByRepo, bool pinByPipeline)
|
||||
{
|
||||
var pinnedRepo = "PinnedRepo";
|
||||
var pinnedPipeline = 1001;
|
||||
|
||||
var config = new DeploymentConfiguration()
|
||||
{
|
||||
Rings = [
|
||||
new RingDefinition() {
|
||||
Name = "A",
|
||||
Tools = new ReadOnlyDictionary<BuildTool, ToolDeployment>(new Dictionary<BuildTool, ToolDeployment> () {
|
||||
{ BuildTool.BuildXL, new ToolDeployment() { Version = "VersionA" } }
|
||||
})
|
||||
}
|
||||
],
|
||||
Overrides = [
|
||||
new DeploymentOverride() {
|
||||
Repository = pinByRepo ? pinnedRepo : "not_ " + pinnedRepo,
|
||||
PipelineIds = pinByPipeline ? [ pinnedPipeline ] : null,
|
||||
Tools = new ReadOnlyDictionary<BuildTool, ToolDeployment>(new Dictionary<BuildTool, ToolDeployment> () {
|
||||
{ BuildTool.BuildXL, new ToolDeployment() { Version = "PinnedVersion" } }
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var mockAdoService = new MockAdoService()
|
||||
{
|
||||
ToolsDirectory = Path.GetTempPath(),
|
||||
RepositoryName = pinnedRepo,
|
||||
PipelineId = pinnedPipeline
|
||||
};
|
||||
|
||||
var expected = pinByRepo ? "PinnedVersion" : "VersionA";
|
||||
Assert.Equal(expected, ConfigurationUtilities.ResolveVersion(config, "A", BuildTool.BuildXL, mockAdoService, new TestLogger()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using BuildToolsInstaller.Utiltiies;
|
||||
|
||||
namespace BuildToolsInstaller.Tests
|
||||
{
|
||||
internal class MockAdoService : IAdoService
|
||||
{
|
||||
public static readonly string OrgName = "testOrg";
|
||||
public bool IsEnabled => true;
|
||||
|
||||
public string CollectionUri => $"https://dev.azure.com/{OrgName}/";
|
||||
|
||||
public required string ToolsDirectory { get; init; }
|
||||
|
||||
public string AccessToken { get; init; } = "<ACCESSTOKEN>";
|
||||
|
||||
public string BuildId { get; init; } = "212121";
|
||||
|
||||
public string RepositoryName { get; init; } = "TestRepo";
|
||||
|
||||
public int PipelineId { get; init; } = 13012;
|
||||
|
||||
public ConcurrentDictionary<string, string> Properties = new();
|
||||
public Task<string?> GetBuildPropertyAsync(string key)
|
||||
{
|
||||
return Task.FromResult(Properties.TryGetValue(key, out var value) ? value : null);
|
||||
}
|
||||
|
||||
public Task SetBuildPropertyAsync(string key, string value)
|
||||
{
|
||||
Properties[key] = value;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void SetVariable(string variableName, string value, bool isReadOnly = true)
|
||||
{
|
||||
// No-Op for tests
|
||||
}
|
||||
|
||||
public bool TryGetOrganizationName([NotNullWhen(true)] out string? organizationName)
|
||||
{
|
||||
organizationName = OrgName;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal class DisabledMockAdoService : IAdoService
|
||||
{
|
||||
public bool IsEnabled => false;
|
||||
|
||||
private Exception Error => new InvalidOperationException("AdoService is disabled");
|
||||
|
||||
public string CollectionUri => throw Error;
|
||||
|
||||
public string ToolsDirectory => throw Error;
|
||||
|
||||
public string AccessToken => throw Error;
|
||||
|
||||
public string BuildId => throw Error;
|
||||
|
||||
public string RepositoryName => throw Error;
|
||||
|
||||
public int PipelineId => throw Error;
|
||||
|
||||
public Task<string?> GetBuildPropertyAsync(string key) => throw Error;
|
||||
|
||||
public Task SetBuildPropertyAsync(string key, string value) => throw Error;
|
||||
|
||||
public void SetVariable(string variableName, string value, bool isReadOnly = true) => throw Error;
|
||||
|
||||
public bool TryGetOrganizationName([NotNullWhen(true)] out string? organizationName) => throw Error;
|
||||
}
|
||||
}
|
|
@ -2,13 +2,8 @@
|
|||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace BuildToolsInstaller.Tests
|
||||
{
|
||||
|
|
Загрузка…
Ссылка в новой задаче