Skip to content

Add sandbox creation for Aspire CLI testing #9550

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions tools/AspireMcpTools/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,135 @@ public static string KillAllAspireCliProcesses()

return resultMessage;
}

[McpServerTool, Description("Creates a sandbox environment for testing the Aspire CLI with locally built packages. Builds Aspire, creates a new directory, and sets up NuGet configuration.")]
public static async Task<string> CreateAspireSandboxAsync()
{
var workspaceRoot = "/workspaces/aspire";
var workspacesDir = "/workspaces";
var artifactsDir = Path.Combine(workspaceRoot, "artifacts");
var packagesDir = Path.Combine(artifactsDir, "packages", "Debug", "Shipping");

try
{
// Step 1: Run ./build.sh -restore -build -pack and wait for completion
var buildResult = await RunProcessAsync("bash", "./build.sh -restore -build -pack /p:InstallBrowsersForPlaywright=false", workspaceRoot).ConfigureAwait(false);
if (buildResult.ExitCode != 0)
{
if (!Directory.Exists(packagesDir))
{
return $"Build failed with exit code {buildResult.ExitCode} and no packages were created. Output: {buildResult.Output}";
}

// Packages exist, so continue despite the non-zero exit code (likely Playwright issues)
}

// Step 2: Run dotnet build on src/Aspire.Cli
var cliDir = Path.Combine(workspaceRoot, "src", "Aspire.Cli");
var cliBuildResult = await RunProcessAsync("dotnet", "build", cliDir).ConfigureAwait(false);
if (cliBuildResult.ExitCode != 0)
{
return $"CLI build failed with exit code {cliBuildResult.ExitCode}. Output: {cliBuildResult.Output}";
}

// Step 3: Create a new folder with unique name in /workspaces directory
var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss", System.Globalization.CultureInfo.InvariantCulture);
var sandboxName = $"aspire-sandbox-{timestamp}";
var sandboxDir = Path.Combine(workspacesDir, sandboxName);
Directory.CreateDirectory(sandboxDir);

// Step 4: Copy /workspaces/aspire/NuGet.config to that directory
var sourceNuGetConfig = Path.Combine(workspaceRoot, "NuGet.config");
var targetNuGetConfig = Path.Combine(sandboxDir, "NuGet.config");
File.Copy(sourceNuGetConfig, targetNuGetConfig);

await UpdateNuGetConfigAsync(targetNuGetConfig, packagesDir).ConfigureAwait(false);

return $"Sandbox created successfully at: {sandboxDir}\nPackages source: {packagesDir}\nIsolated global packages folder: {Path.Combine(sandboxDir, "packages")}\nNuGet.config configured to use locally built Aspire packages.\n\nTo use this sandbox:\n1. cd {sandboxDir}\n2. Use the locally built aspire CLI from: {Path.Combine(workspaceRoot, "src", "Aspire.Cli")}\n3. Any Aspire projects created here will use the locally built packages";
}
catch (Exception ex)
{
return $"Failed to create sandbox: {ex.Message}";
}
}

private static async Task<(int ExitCode, string Output)> RunProcessAsync(string fileName, string arguments, string workingDirectory)
{
using var process = new System.Diagnostics.Process();
process.StartInfo.FileName = fileName;
process.StartInfo.Arguments = arguments;
process.StartInfo.WorkingDirectory = workingDirectory;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;

var outputBuilder = new System.Text.StringBuilder();
var errorBuilder = new System.Text.StringBuilder();

process.OutputDataReceived += (sender, e) =>
{
if (e.Data is not null)
{
outputBuilder.AppendLine(e.Data);
}
};
process.ErrorDataReceived += (sender, e) =>
{
if (e.Data is not null)
{
errorBuilder.AppendLine(e.Data);
}
};

process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();

await process.WaitForExitAsync().ConfigureAwait(false);

var combinedOutput = outputBuilder.ToString() + errorBuilder.ToString();
return (process.ExitCode, combinedOutput);
}

private static async Task UpdateNuGetConfigAsync(string nugetConfigPath, string packagesPath)
{
var content = await File.ReadAllTextAsync(nugetConfigPath).ConfigureAwait(false);

// Create global packages folder path relative to the sandbox directory
var sandboxDir = Path.GetDirectoryName(nugetConfigPath)!;
var globalPackagesPath = Path.Combine(sandboxDir, "packages");
Directory.CreateDirectory(globalPackagesPath);

// Add global packages folder configuration after <configuration> tag
var configurationStartTag = "<configuration>";
var globalPackagesConfig =
$"""
<config>
<add key="globalPackagesFolder" value="{globalPackagesPath}" />
</config>
""";

content = content.Replace(configurationStartTag, $"{configurationStartTag}\n{globalPackagesConfig}");

// Add local-packages source after the existing sources
var packageSourcesEndTag = "</packageSources>";
var localPackageSource = $""" <add key="local-packages" value="{packagesPath}" />""";

content = content.Replace(packageSourcesEndTag, $"{localPackageSource}\n {packageSourcesEndTag}");

// Add package source mapping for Aspire* and Microsoft.Extensions.ServiceDiscovery* to local-packages
var packageSourceMappingEndTag = "</packageSourceMapping>";
var localPackageMapping =
"""
<packageSource key="local-packages">
<package pattern="Aspire*" />
<package pattern="Microsoft.Extensions.ServiceDiscovery*" />
</packageSource>
""";

content = content.Replace(packageSourceMappingEndTag, $"{localPackageMapping} {packageSourceMappingEndTag}");

await File.WriteAllTextAsync(nugetConfigPath, content).ConfigureAwait(false);
}
}
Loading