// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using System.Text.RegularExpressions;
using Hex1b;
namespace Aspire.Tests.Shared;
/// <summary>
/// Specifies how the Aspire CLI should be installed in E2E test environments.
/// </summary>
internal enum CliInstallMode
{
/// <summary>
/// Extract a localhive archive (.tar.gz) into the test environment.
/// Set via ASPIRE_E2E_ARCHIVE.
/// </summary>
LocalHive,
/// <summary>
/// Use a CLI that has already been installed into the test environment.
/// Intended for non-Docker scenarios where CI preinstalls the current build.
/// </summary>
Preinstalled,
/// <summary>
/// Install from PR build artifacts using get-aspire-cli-pr.sh.
/// </summary>
PullRequest,
/// <summary>
/// Install from pre-downloaded artifacts in a local directory using get-aspire-cli-pr.sh with --local-dir.
/// Used for same-workflow scenarios (e.g., quarantine/outerloop) where artifacts are already on disk.
/// </summary>
LocalArchive,
/// <summary>
/// Install via get-aspire-cli.sh with optional --quality or --version.
/// Covers GA releases, daily builds, preview, and explicit versions.
/// </summary>
InstallScript,
/// <summary>
/// Install via <c>dotnet tool install --global Aspire.Cli</c>.
/// Supports published NuGet feeds and local nupkg sources.
/// Set via ASPIRE_E2E_DOTNET_TOOL or ASPIRE_E2E_DOTNET_TOOL_SOURCE.
/// </summary>
DotnetTool,
}
/// <summary>
/// Shared shell commands used by the strategy-aware CLI install helpers.
/// </summary>
internal static class AspireCliShellCommandHelpers
{
internal const string NumberedPromptSetupCommand = "CMDCOUNT=0; PROMPT_COMMAND='s=$?;((CMDCOUNT++));PS1=\"[$CMDCOUNT $([ $s -eq 0 ] && echo OK || echo ERR:$s)] \\$ \"'";
internal const string CommonAspireEnvironmentAssignments = "ASPIRE_PLAYGROUND=true TERM=xterm DOTNET_CLI_TELEMETRY_OPTOUT=true DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true DOTNET_GENERATE_ASPNET_CERTIFICATE=false";
internal const string AspireCliVersionCommand = "aspire --version";
internal const string DockerInstallScriptCommandPrefix = "/opt/aspire-scripts/get-aspire-cli.sh";
internal const string DockerPullRequestInstallCommandPrefix = "/opt/aspire-scripts/get-aspire-cli-pr.sh";
internal const string DockerNuGetConfigPath = "/opt/aspire-scripts/NuGet.config";
internal const string MainInstallScriptCommandPrefix = "curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli.sh | bash -s --";
internal const string MainPullRequestInstallCommandPrefix = "curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s --";
internal const string AkaMsInstallScriptCommandPrefix = "curl -fsSL https://aka.ms/aspire/get/install.sh | bash -s --";
private static readonly string[] s_configureLocalHiveCommands =
[
"aspire config set channel local -g",
"SDK_VER=$(ls ~/.aspire/hives/local/packages/Aspire.Hosting.*.nupkg 2>/dev/null | head -1 | sed 's/.*Aspire\\.Hosting\\.//;s/\\.nupkg//') && aspire config set sdk.version \"$SDK_VER\" -g"
];
internal static string GetPrepareAspireEnvironmentCommand()
{
return $"export {CommonAspireEnvironmentAssignments}";
}
internal static string GetSourceAspireEnvironmentCommand(bool includeBundlePath)
{
return includeBundlePath
? $"export PATH=~/.aspire/bin:~/.aspire:$PATH {CommonAspireEnvironmentAssignments}"
: $"export PATH=~/.aspire/bin:$PATH {CommonAspireEnvironmentAssignments}";
}
internal static string GetExtractLocalHiveArchiveCommand(string archivePath)
{
return $"mkdir -p ~/.aspire && tar -xzf {QuoteBashArg(archivePath)} -C ~/.aspire 2>/dev/null";
}
internal static IEnumerable<string> GetConfigureLocalHiveCommands()
{
return s_configureLocalHiveCommands;
}
internal static string GetInstallScriptCommand(CliInstallStrategy strategy, string commandPrefix)
{
return $"{commandPrefix}{GetInstallScriptArgs(strategy)}";
}
internal static string GetPullRequestInstallCommand(int prNumber, string commandPrefix)
{
return $"{commandPrefix} {GetPullRequestInstallArgs(prNumber)}";
}
internal static string GetBundlePullRequestInstallCommand(int prNumber)
{
return
$"ref=$(gh api repos/microsoft/aspire/pulls/{prNumber} --jq '.head.sha') && " +
$"curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/$ref/eng/scripts/get-aspire-cli-pr.sh | bash -s -- {GetPullRequestInstallArgs(prNumber)}";
}
internal static string GetInstallScriptArgs(CliInstallStrategy strategy)
{
if (strategy.Quality is not null)
{
return $" --quality {strategy.Quality.Value.ToString().ToLowerInvariant()}";
}
return strategy.Version is not null
? $" --version {strategy.Version}"
: "";
}
internal static string GetPullRequestInstallArgs(int prNumber)
{
return prNumber.ToString(CultureInfo.InvariantCulture);
}
internal static string GetLocalArchiveInstallCommand(string localDir, string commandPrefix)
{
return $"{commandPrefix} --local-dir {QuoteBashArg(localDir)}";
}
internal static string GetLocalArchiveInstallCommandFromCurrentRef(string localDir)
{
var sha = Environment.GetEnvironmentVariable("GITHUB_SHA") ?? "main";
if (!Regex.IsMatch(sha, @"^[0-9a-fA-F]{7,40}$") && sha != "main")
{
throw new InvalidOperationException($"GITHUB_SHA contains an unexpected value: '{sha}'. Expected a hex commit SHA or 'main'.");
}
return $"curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/{sha}/eng/scripts/get-aspire-cli-pr.sh | bash -s -- --local-dir {QuoteBashArg(localDir)}";
}
internal static string QuoteBashArg(string value)
{
return $"'{value.Replace("'", "'\"'\"'")}'";
}
internal static string GetDotnetToolInstallCommand(CliInstallStrategy strategy)
{
return $"dotnet tool install {GetDotnetToolInstallArgs(strategy, strategy.NupkgSourcePath)}";
}
internal static string GetDotnetToolInstallCommandInDocker(CliInstallStrategy strategy)
{
var nupkgSourcePath = strategy.NupkgSourcePath is not null ? "/tmp/aspire-nupkg-source" : null;
var nuGetConfigPath = strategy.RequiresDotnetToolNuGetConfig ? DockerNuGetConfigPath : null;
return $"dotnet tool install {GetDotnetToolInstallArgs(strategy, nupkgSourcePath, nuGetConfigPath)}";
}
internal static string GetDotnetAddPackageCommand(string projectPath, string packageId)
{
return
$"PKG={QuoteBashArg(packageId)}; " +
$"TARGET={QuoteBashArg(projectPath)}; " +
"PKG_PATH=$(find \"$HOME/.aspire/hives\" -path \"*/packages/$PKG.[0-9]*.nupkg\" -type f 2>/dev/null | sort -V | tail -n 1); " +
"if [ -n \"$PKG_PATH\" ]; then " +
"PKG_FILE=$(basename \"$PKG_PATH\"); " +
"PKG_VERSION=${PKG_FILE#\"$PKG.\"}; " +
"PKG_VERSION=${PKG_VERSION%.nupkg}; " +
"dotnet add \"$TARGET\" package \"$PKG\" --version \"$PKG_VERSION\"; " +
"else dotnet add \"$TARGET\" package \"$PKG\" --prerelease; fi";
}
private static string GetDotnetToolInstallArgs(CliInstallStrategy strategy, string? nupkgSourcePath, string? nuGetConfigPath = null)
{
var args = "--global Aspire.Cli";
if (strategy.Version is not null)
{
args += $" --version {strategy.Version}";
}
else if (strategy.IncludePrerelease)
{
args += " --prerelease";
}
if (nupkgSourcePath is not null)
{
args += $" --add-source {QuoteBashArg(nupkgSourcePath)}";
}
if (nuGetConfigPath is not null)
{
args += $" --configfile {QuoteBashArg(nuGetConfigPath)}";
}
return args;
}
}
/// <summary>
/// Quality levels for the Aspire CLI install script.
/// </summary>
internal enum CliInstallQuality
{
/// <summary>Daily development builds.</summary>
Dev,
/// <summary>Staging/preview builds.</summary>
Staging,
/// <summary>Official release builds.</summary>
Release,
}
/// <summary>
/// Encapsulates how the Aspire CLI is detected, configured, and installed for test scenarios.
/// </summary>
internal sealed class CliInstallStrategy
{
internal const string CliArchiveDirEnvironmentVariableName = "ASPIRE_E2E_CLI_ARCHIVE_DIR";
internal const string UbuntuAptMirrorBuildArgName = "UBUNTU_APT_MIRROR";
internal const string UbuntuAptMirrorEnvironmentVariableName = "ASPIRE_E2E_UBUNTU_APT_MIRROR";
private const string PreinstalledEnvironmentVariableName = "ASPIRE_E2E_PREINSTALLED";
/// <summary>
/// The install mode.
/// </summary>
public CliInstallMode Mode { get; }
/// <summary>
/// For LocalHive: the path to the .tar.gz archive on the host.
/// </summary>
public string? ArchivePath { get; }
/// <summary>
/// For InstallScript: the quality level.
/// </summary>
public CliInstallQuality? Quality { get; }
/// <summary>
/// For InstallScript: a specific version.
/// </summary>
public string? Version { get; }
/// <summary>
/// For LocalArchive: the directory containing pre-downloaded CLI archives and NuGet packages.
/// </summary>
public string? ArchiveDir { get; }
/// <summary>
/// For DotnetTool: path to directory containing nupkg files (local feed).
/// </summary>
public string? NupkgSourcePath { get; }
/// <summary>
/// For DotnetTool: include prerelease package versions when installing the latest available version.
/// </summary>
public bool IncludePrerelease { get; }
/// <summary>
/// The expected CLI version after installation, when known.
/// Set automatically for modes where the version is deterministic (LocalArchive, DotnetTool local source, explicit version).
/// Used by post-install verification to assert the correct CLI binary was installed.
/// </summary>
public string? ExpectedVersion { get; }
/// <summary>
/// Gets whether the Docker dotnet tool install command needs the generated NuGet.config for internal/prerelease feeds.
/// </summary>
public bool RequiresDotnetToolNuGetConfig =>
Mode is CliInstallMode.DotnetTool &&
NupkgSourcePath is null &&
(IncludePrerelease || IsPrereleaseVersion(Version));
private CliInstallStrategy(CliInstallMode mode, string? archivePath = null, CliInstallQuality? quality = null, string? version = null, string? archiveDir = null, string? nupkgSourcePath = null, bool includePrerelease = false, string? expectedVersion = null)
{
Mode = mode;
ArchivePath = archivePath;
Quality = quality;
Version = version;
ArchiveDir = archiveDir;
NupkgSourcePath = nupkgSourcePath;
IncludePrerelease = includePrerelease;
ExpectedVersion = expectedVersion;
}
/// <summary>
/// Creates a LocalHive strategy from an archive path.
/// </summary>
public static CliInstallStrategy FromLocalHive(string archivePath)
{
if (!File.Exists(archivePath))
{
throw new FileNotFoundException($"LocalHive archive not found: {archivePath}");
}
return new CliInstallStrategy(CliInstallMode.LocalHive, archivePath: archivePath);
}
/// <summary>
/// Creates a strategy for a CLI that is already installed in the test environment.
/// </summary>
public static CliInstallStrategy Preinstalled()
{
return new CliInstallStrategy(CliInstallMode.Preinstalled);
}
/// <summary>
/// Creates an InstallScript strategy targeting a quality level.
/// </summary>
public static CliInstallStrategy FromQuality(CliInstallQuality quality)
{
return new CliInstallStrategy(CliInstallMode.InstallScript, quality: quality);
}
/// <summary>
/// Creates an InstallScript strategy targeting a specific version.
/// </summary>
public static CliInstallStrategy FromVersion(string version)
{
if (!CliPackageDiscovery.IsValidVersion(version))
{
throw new ArgumentException($"Invalid version format: '{version}'. Must contain only alphanumeric characters, dots, and dashes.", nameof(version));
}
return new CliInstallStrategy(CliInstallMode.InstallScript, version: version);
}
/// <summary>
/// Creates a PullRequest strategy when the environment contains PR metadata.
/// </summary>
public static CliInstallStrategy FromPullRequest()
{
var prNumber = Environment.GetEnvironmentVariable("GITHUB_PR_NUMBER");
var headSha = Environment.GetEnvironmentVariable("GITHUB_PR_HEAD_SHA");
if (string.IsNullOrEmpty(prNumber) || string.IsNullOrEmpty(headSha))
{
throw new InvalidOperationException("PullRequest strategy requires GITHUB_PR_NUMBER and GITHUB_PR_HEAD_SHA to be set.");
}
return new CliInstallStrategy(CliInstallMode.PullRequest);
}
/// <summary>
/// Creates a LocalArchive strategy from a directory containing pre-downloaded CLI archives and NuGet packages.
/// Automatically extracts the expected CLI version from the <c>Aspire.Cli.{version}.nupkg</c> file in the directory.
/// </summary>
public static CliInstallStrategy FromLocalArchive(string archiveDir)
{
if (!Directory.Exists(archiveDir))
{
throw new DirectoryNotFoundException($"LocalArchive directory not found: {archiveDir}");
}
var expectedVersion = CliPackageDiscovery.TryFindAspireCliPointerPackage(archiveDir)?.Version;
return new CliInstallStrategy(CliInstallMode.LocalArchive, archiveDir: archiveDir, expectedVersion: expectedVersion);
}
/// <summary>
/// Creates an InstallScript strategy for the latest GA release.
/// </summary>
public static CliInstallStrategy LatestGa()
{
return new CliInstallStrategy(CliInstallMode.InstallScript);
}
/// <summary>
/// Creates a DotnetTool strategy to install from a published NuGet feed.
/// </summary>
public static CliInstallStrategy FromDotnetTool(string? version = null, bool includePrerelease = false)
{
if (version is not null && !CliPackageDiscovery.IsValidVersion(version))
{
throw new ArgumentException($"Invalid version format: '{version}'. Must contain only alphanumeric characters, dots, and dashes.", nameof(version));
}
return new CliInstallStrategy(CliInstallMode.DotnetTool, version: version, includePrerelease: version is null && includePrerelease, expectedVersion: version);
}
/// <summary>
/// Creates a DotnetTool strategy for a published NuGet feed.
/// </summary>
public static CliInstallStrategy FromPublishedDotnetToolFeed(string? version, CliInstallQuality? quality)
{
var includePrerelease = ShouldIncludePrereleaseForPublishedDotnetTool(version, quality);
return FromDotnetTool(version, includePrerelease);
}
/// <summary>
/// Creates a DotnetTool strategy to install from a local directory of nupkg files.
/// </summary>
public static CliInstallStrategy FromDotnetToolLocalSource(string nupkgSourcePath, string version)
{
if (!Directory.Exists(nupkgSourcePath))
{
throw new DirectoryNotFoundException($"Nupkg source directory not found: {nupkgSourcePath}");
}
if (!CliPackageDiscovery.IsValidVersion(version))
{
throw new ArgumentException($"Invalid version format: '{version}'. Must contain only alphanumeric characters, dots, and dashes.", nameof(version));
}
return new CliInstallStrategy(CliInstallMode.DotnetTool, version: version, nupkgSourcePath: nupkgSourcePath, expectedVersion: version);
}
/// <summary>
/// Creates a DotnetTool strategy to install from a local directory of nupkg files, inferring
/// the tool package version from the <c>Aspire.Cli.{version}.nupkg</c> package.
/// </summary>
public static CliInstallStrategy FromDotnetToolLocalSource(string nupkgSourcePath)
{
if (!Directory.Exists(nupkgSourcePath))
{
throw new DirectoryNotFoundException($"Nupkg source directory not found: {nupkgSourcePath}");
}
var version = CliPackageDiscovery.FindAspireCliPointerPackage(nupkgSourcePath).Version;
return FromDotnetToolLocalSource(nupkgSourcePath, version);
}
/// <summary>
/// Auto-detect the install strategy from the environment.
/// Priority:
/// 1. ASPIRE_E2E_ARCHIVE → LocalHive
/// 2. ASPIRE_E2E_DOTNET_TOOL_SOURCE → DotnetTool with local nupkg source
/// 3. ASPIRE_E2E_DOTNET_TOOL=true → DotnetTool from published feed
/// 4. ASPIRE_E2E_QUALITY → InstallScript with quality
/// 5. ASPIRE_E2E_VERSION → InstallScript with version
/// 6. ASPIRE_E2E_PREINSTALLED → Preinstalled
/// 7. ASPIRE_E2E_CLI_ARCHIVE_DIR → LocalArchive
/// 8. GITHUB_PR_NUMBER + GITHUB_PR_HEAD_SHA → PullRequest
/// 9. CI/GITHUB_ACTIONS → InstallScript (dev/daily)
/// 10. Local fallback → InstallScript (latest GA)
/// </summary>
/// <param name="log">Optional log callback (e.g. <c>output.WriteLine</c>) for tracing the detection logic.</param>
public static CliInstallStrategy Detect(Action<string>? log = null)
{
log?.Invoke("CLI install strategy detection starting...");
var archivePath = Environment.GetEnvironmentVariable("ASPIRE_E2E_ARCHIVE");
if (!string.IsNullOrEmpty(archivePath))
{
log?.Invoke($" → Selected: LocalHive (ASPIRE_E2E_ARCHIVE={archivePath})");
return FromLocalHive(archivePath);
}
var dotnetToolSource = Environment.GetEnvironmentVariable("ASPIRE_E2E_DOTNET_TOOL_SOURCE");
if (!string.IsNullOrEmpty(dotnetToolSource))
{
var toolVersion = Environment.GetEnvironmentVariable("ASPIRE_E2E_VERSION");
var strategy = !string.IsNullOrEmpty(toolVersion)
? FromDotnetToolLocalSource(dotnetToolSource, toolVersion)
: FromDotnetToolLocalSource(dotnetToolSource);
log?.Invoke($" → Selected: DotnetTool local source (ASPIRE_E2E_DOTNET_TOOL_SOURCE={dotnetToolSource}, version={strategy.Version})");
return strategy;
}
var dotnetTool = Environment.GetEnvironmentVariable("ASPIRE_E2E_DOTNET_TOOL");
if (string.Equals(dotnetTool, "true", StringComparison.OrdinalIgnoreCase) ||
string.Equals(dotnetTool, "1", StringComparison.OrdinalIgnoreCase))
{
var toolVersion = Environment.GetEnvironmentVariable("ASPIRE_E2E_VERSION");
var quality = GetQualityFromEnvironment();
var strategy = FromPublishedDotnetToolFeed(toolVersion, quality);
var versionDescription = toolVersion ?? (strategy.IncludePrerelease ? "(latest prerelease)" : "(latest stable)");
log?.Invoke($" → Selected: DotnetTool published feed (ASPIRE_E2E_DOTNET_TOOL={dotnetTool}, version={versionDescription})");
return strategy;
}
var qualityStr = GetQualityFromEnvironment();
if (qualityStr is not null)
{
log?.Invoke($" → Selected: InstallScript quality={qualityStr}");
return FromQuality(qualityStr.Value);
}
var version = Environment.GetEnvironmentVariable("ASPIRE_E2E_VERSION");
if (!string.IsNullOrEmpty(version))
{
log?.Invoke($" → Selected: InstallScript version={version}");
return FromVersion(version);
}
var preinstalled = Environment.GetEnvironmentVariable(PreinstalledEnvironmentVariableName);
if (string.Equals(preinstalled, "true", StringComparison.OrdinalIgnoreCase) ||
string.Equals(preinstalled, "1", StringComparison.OrdinalIgnoreCase))
{
log?.Invoke(" → Selected: Preinstalled");
return Preinstalled();
}
var archiveDir = Environment.GetEnvironmentVariable(CliArchiveDirEnvironmentVariableName);
if (!string.IsNullOrEmpty(archiveDir))
{
log?.Invoke($" → Selected: LocalArchive ({CliArchiveDirEnvironmentVariableName}={archiveDir})");
return FromLocalArchive(archiveDir);
}
var prNumber = Environment.GetEnvironmentVariable("GITHUB_PR_NUMBER");
var headSha = Environment.GetEnvironmentVariable("GITHUB_PR_HEAD_SHA");
if (!string.IsNullOrEmpty(prNumber) && !string.IsNullOrEmpty(headSha))
{
log?.Invoke($" → Selected: PullRequest (PR #{prNumber}, SHA={headSha})");
return FromPullRequest();
}
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")) ||
!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS")))
{
log?.Invoke(" → Selected: InstallScript (CI fallback, quality=dev)");
return FromQuality(CliInstallQuality.Dev);
}
log?.Invoke(" → Selected: InstallScript (local fallback, latest GA)");
return LatestGa();
}
/// <summary>
/// Configures a Docker container with the volumes, build args, and environment variables needed for this install mode.
/// </summary>
public void ConfigureContainer(DockerContainerOptions config)
{
config.BuildArgs["SKIP_SOURCE_BUILD"] = "true";
ConfigureUbuntuAptMirrorBuildArg(config.BuildArgs);
switch (Mode)
{
case CliInstallMode.LocalHive:
config.Volumes.Add($"{ArchivePath}:/tmp/aspire-localhive.tar.gz:ro");
break;
case CliInstallMode.Preinstalled:
throw new InvalidOperationException("Preinstalled CLI mode is only supported for non-Docker test environments.");
case CliInstallMode.PullRequest:
var ghToken = Environment.GetEnvironmentVariable("GH_TOKEN");
if (!string.IsNullOrEmpty(ghToken))
{
config.Environment["GH_TOKEN"] = ghToken;
}
config.Environment["GITHUB_PR_NUMBER"] = Environment.GetEnvironmentVariable("GITHUB_PR_NUMBER") ?? "";
config.Environment["GITHUB_PR_HEAD_SHA"] = Environment.GetEnvironmentVariable("GITHUB_PR_HEAD_SHA") ?? "";
break;
case CliInstallMode.LocalArchive:
if (string.IsNullOrEmpty(ArchiveDir))
{
throw new InvalidOperationException("LocalArchive mode requires ArchiveDir to be set.");
}
config.Volumes.Add($"{ArchiveDir}:/tmp/aspire-cli-archives:ro");
break;
case CliInstallMode.InstallScript:
break;
case CliInstallMode.DotnetTool:
if (NupkgSourcePath is not null)
{
config.Volumes.Add($"{NupkgSourcePath}:/tmp/aspire-nupkg-source:ro");
}
break;
default:
throw new InvalidOperationException($"Unknown install mode: {Mode}");
}
}
internal static void ConfigureUbuntuAptMirrorBuildArg(IDictionary<string, string> buildArgs)
{
var ubuntuAptMirror = Environment.GetEnvironmentVariable(UbuntuAptMirrorEnvironmentVariableName);
if (!string.IsNullOrWhiteSpace(ubuntuAptMirror))
{
buildArgs[UbuntuAptMirrorBuildArgName] = ubuntuAptMirror;
}
}
/// <summary>
/// Returns a description of this strategy for test output logging.
/// </summary>
public override string ToString()
{
var description = Mode switch
{
CliInstallMode.LocalHive => $"LocalHive ({ArchivePath})",
CliInstallMode.Preinstalled => "Preinstalled (~/.aspire)",
CliInstallMode.PullRequest => $"PullRequest (PR #{Environment.GetEnvironmentVariable("GITHUB_PR_NUMBER")})",
CliInstallMode.LocalArchive => $"LocalArchive ({ArchiveDir})",
CliInstallMode.InstallScript when Quality is not null => $"InstallScript (--quality {Quality.Value.ToString().ToLowerInvariant()})",
CliInstallMode.InstallScript when Version is not null => $"InstallScript (--version {Version})",
CliInstallMode.InstallScript => "InstallScript (latest GA)",
CliInstallMode.DotnetTool when NupkgSourcePath is not null => $"DotnetTool (local: {NupkgSourcePath}, --version {Version})",
CliInstallMode.DotnetTool when Version is not null => $"DotnetTool (--version {Version})",
CliInstallMode.DotnetTool when IncludePrerelease => "DotnetTool (latest prerelease)",
CliInstallMode.DotnetTool => "DotnetTool (latest)",
_ => Mode.ToString(),
};
return ExpectedVersion is not null
? $"{description} [expected={ExpectedVersion}]"
: description;
}
internal static CliInstallQuality? GetQualityFromEnvironment()
{
var qualityStr = Environment.GetEnvironmentVariable("ASPIRE_E2E_QUALITY");
if (string.IsNullOrEmpty(qualityStr))
{
return null;
}
if (Enum.TryParse<CliInstallQuality>(qualityStr, ignoreCase: true, out var quality))
{
return quality;
}
throw new ArgumentException($"Invalid ASPIRE_E2E_QUALITY value: '{qualityStr}'. Must be one of: {string.Join(", ", Enum.GetNames<CliInstallQuality>())}");
}
private static bool ShouldIncludePrereleaseForPublishedDotnetTool(string? version, CliInstallQuality? quality)
{
return string.IsNullOrEmpty(version) && quality is CliInstallQuality.Dev or CliInstallQuality.Staging;
}
private static bool IsPrereleaseVersion(string? version)
{
return version?.Contains('-', StringComparison.Ordinal) == true;
}
}