Roslyn-Stone / build.cake
dylanlangston's picture
Add files using upload-large-folder tool
e462aae verified
///////////////////////////////////////////////////////////////////////////////
// ARGUMENTS
///////////////////////////////////////////////////////////////////////////////
var target = Argument("target", "Default");
var configuration = Argument("configuration", "Debug");
var version = Argument("version", "1.0.0");
///////////////////////////////////////////////////////////////////////////////
// SETUP / TEARDOWN
///////////////////////////////////////////////////////////////////////////////
Setup(ctx =>
{
Information("Running tasks...");
Information($"Configuration: {configuration}");
Information($"Version: {version}");
});
Teardown(ctx =>
{
Information("Finished running tasks.");
});
///////////////////////////////////////////////////////////////////////////////
// TASKS
///////////////////////////////////////////////////////////////////////////////
Task("Clean")
.Does(() =>
{
CleanDirectories("./src/**/bin");
CleanDirectories("./src/**/obj");
CleanDirectories("./tests/**/bin");
CleanDirectories("./tests/**/obj");
CleanDirectories("./artifacts");
});
Task("Restore")
.IsDependentOn("Clean")
.Does(() =>
{
DotNetRestore("./RoslynStone.sln");
});
Task("Build")
.IsDependentOn("Restore")
.Does(() =>
{
DotNetBuild("./RoslynStone.sln", new DotNetBuildSettings
{
Configuration = configuration,
NoRestore = true
});
});
Task("Format")
.Description("Format code with CSharpier")
.Does(() =>
{
StartProcess("csharpier", new ProcessSettings
{
Arguments = "format ."
});
});
Task("Format-Check")
.Description("Check code formatting with CSharpier")
.Does(() =>
{
var exitCode = StartProcess("csharpier", new ProcessSettings
{
Arguments = "check ."
});
if (exitCode != 0)
{
throw new Exception("Code formatting issues found. Run 'dotnet cake --target=Format' to fix.");
}
});
Task("Python-Format")
.Description("Format Python code with Ruff")
.Does(() =>
{
StartProcess("bash", new ProcessSettings
{
Arguments = "./scripts/format-python.sh"
});
});
Task("Python-Check")
.Description("Check Python code quality (Ruff + mypy)")
.Does(() =>
{
var exitCode = StartProcess("bash", new ProcessSettings
{
Arguments = "./scripts/check-python-quality.sh"
});
if (exitCode != 0)
{
throw new Exception("Python quality checks failed. Run 'dotnet cake --target=Python-Format' to fix formatting issues.");
}
});
Task("Inspect")
.Description("Run ReSharper code inspections")
.IsDependentOn("Build")
.Does(() =>
{
var reportPath = "./artifacts/resharper-report.xml";
EnsureDirectoryExists("./artifacts");
StartProcess("jb", new ProcessSettings
{
Arguments = $"inspectcode RoslynStone.sln --output={reportPath} --severity=WARNING"
});
Information($"ReSharper inspection report generated: {reportPath}");
});
Task("Test")
.IsDependentOn("Build")
.Does(() =>
{
DotNetTest("./RoslynStone.sln", new DotNetTestSettings
{
Configuration = configuration,
NoRestore = true,
NoBuild = true,
Loggers = new[] { "console;verbosity=normal" }
});
});
Task("Test-Coverage")
.Description("Run tests with code coverage")
.IsDependentOn("Build")
.Does(() =>
{
EnsureDirectoryExists("./artifacts/coverage");
DotNetTest("./RoslynStone.sln", new DotNetTestSettings
{
Configuration = configuration,
NoRestore = true,
NoBuild = true,
Loggers = new[] { "console;verbosity=normal" },
ArgumentCustomization = args => args
.Append("--collect:\"XPlat Code Coverage\"")
.Append("--results-directory ./artifacts/coverage")
});
// Parse coverage results and check branch coverage
var coverageFiles = GetFiles("./artifacts/coverage/**/coverage.cobertura.xml");
if (coverageFiles.Any())
{
// Aggregate coverage from all files to get total metrics
double totalLinesCovered = 0;
double totalLinesValid = 0;
double totalBranchesCovered = 0;
double totalBranchesValid = 0;
foreach (var coverageFile in coverageFiles)
{
var xml = System.Xml.Linq.XDocument.Load(coverageFile.FullPath);
var coverage = xml.Root;
if (coverage == null) continue;
var linesCoveredAttr = coverage.Attribute("lines-covered");
var linesValidAttr = coverage.Attribute("lines-valid");
var branchesCoveredAttr = coverage.Attribute("branches-covered");
var branchesValidAttr = coverage.Attribute("branches-valid");
if (linesCoveredAttr != null && linesValidAttr != null &&
branchesCoveredAttr != null && branchesValidAttr != null)
{
totalLinesCovered += double.Parse(linesCoveredAttr.Value);
totalLinesValid += double.Parse(linesValidAttr.Value);
totalBranchesCovered += double.Parse(branchesCoveredAttr.Value);
totalBranchesValid += double.Parse(branchesValidAttr.Value);
}
}
if (totalLinesValid == 0)
{
Warning("Unable to parse coverage report: no valid lines found");
return;
}
var lineCoverage = (totalLinesCovered / totalLinesValid) * 100;
var branchCoverage = totalBranchesValid > 0 ? (totalBranchesCovered / totalBranchesValid) * 100 : 0;
Information($"Line Coverage: {lineCoverage:F2}% ({totalLinesCovered}/{totalLinesValid} lines)");
Information($"Branch Coverage: {branchCoverage:F2}% ({totalBranchesCovered}/{totalBranchesValid} branches)");
// Enforce minimum coverage thresholds
const double MinBranchCoverage = 75.0;
const double MinLineCoverage = 80.0;
if (branchCoverage < MinBranchCoverage)
{
Warning($"⚠️ Branch coverage ({branchCoverage:F2}%) is below the minimum threshold of {MinBranchCoverage}%");
// Note: Not failing the build yet to allow incremental improvements
// throw new Exception($"Branch coverage ({branchCoverage:F2}%) is below the minimum threshold of {MinBranchCoverage}%");
}
else
{
Information($"✅ Branch coverage meets the minimum threshold");
}
if (lineCoverage < MinLineCoverage)
{
Warning($"⚠️ Line coverage ({lineCoverage:F2}%) is below the minimum threshold of {MinLineCoverage}%");
}
else
{
Information($"✅ Line coverage meets the minimum threshold");
}
}
});
Task("Test-Coverage-Report")
.Description("Generate HTML coverage report using ReportGenerator")
.IsDependentOn("Test-Coverage")
.Does(() =>
{
var coverageFiles = GetFiles("./artifacts/coverage/**/coverage.cobertura.xml");
if (coverageFiles.Any())
{
EnsureDirectoryExists("./artifacts/coverage-report");
var settings = new ProcessSettings
{
Arguments = new ProcessArgumentBuilder()
.Append($"-reports:{string.Join(";", coverageFiles.Select(f => f.FullPath))}")
.Append("-targetdir:./artifacts/coverage-report")
.Append("-reporttypes:Html;Badges")
};
StartProcess("reportgenerator", settings);
Information("Coverage report generated at ./artifacts/coverage-report/index.html");
}
else
{
Warning("No coverage files found to generate report");
}
});
Task("Benchmark")
.Description("Run benchmarks")
.IsDependentOn("Build")
.Does(() =>
{
Information("Running benchmarks...");
Information("Note: Benchmarks can take several minutes to complete.");
var benchmarkProject = "./tests/RoslynStone.Benchmarks/RoslynStone.Benchmarks.csproj";
DotNetRun(benchmarkProject, new DotNetRunSettings
{
Configuration = "Release",
NoBuild = false,
ArgumentCustomization = args => args.Append("--filter * --artifacts ./artifacts/benchmarks")
});
Information("Benchmark results saved to ./artifacts/benchmarks");
});
Task("Load-Test")
.Description("Run load tests against a running HTTP server")
.IsDependentOn("Build")
.Does(() =>
{
Information("Running load tests...");
Information("Note: Ensure the API server is running in HTTP mode:");
Information(" cd src/RoslynStone.Api && MCP_TRANSPORT=http dotnet run");
Information("");
var loadTestProject = "./tests/RoslynStone.LoadTests/RoslynStone.LoadTests.csproj";
try
{
DotNetRun(loadTestProject, new DotNetRunSettings
{
Configuration = configuration,
NoBuild = true
});
}
catch (Exception ex)
{
Warning($"Load test failed: {ex.Message}");
Warning("Make sure the server is running with: MCP_TRANSPORT=http dotnet run");
}
});
Task("Pack")
.Description("Create NuGet packages")
.IsDependentOn("Build")
.Does(() =>
{
EnsureDirectoryExists("./artifacts/packages");
var projects = new[]
{
"./src/RoslynStone.Core/RoslynStone.Core.csproj",
"./src/RoslynStone.Infrastructure/RoslynStone.Infrastructure.csproj"
};
foreach (var project in projects)
{
DotNetPack(project, new DotNetPackSettings
{
Configuration = configuration,
OutputDirectory = "./artifacts/packages",
NoRestore = true,
NoBuild = true,
ArgumentCustomization = args => args
.Append($"/p:Version={version}")
.Append($"/p:PackageVersion={version}")
});
}
Information($"NuGet packages created in ./artifacts/packages");
});
Task("Publish-NuGet")
.Description("Publish NuGet packages to NuGet.org")
.IsDependentOn("Pack")
.Does(() =>
{
var apiKey = EnvironmentVariable("NUGET_API_KEY");
if (string.IsNullOrEmpty(apiKey))
{
throw new Exception("NUGET_API_KEY environment variable not set");
}
var packages = GetFiles("./artifacts/packages/*.nupkg");
foreach (var package in packages)
{
DotNetNuGetPush(package.FullPath, new DotNetNuGetPushSettings
{
Source = "https://api.nuget.org/v3/index.json",
ApiKey = apiKey
});
}
});
Task("CI")
.Description("Run all CI tasks: Format check (C# + Python), Build, Inspect, Test with Coverage")
.IsDependentOn("Format-Check")
.IsDependentOn("Python-Check")
.IsDependentOn("Inspect")
.IsDependentOn("Test-Coverage");
Task("Default")
.IsDependentOn("Build")
.IsDependentOn("Test");
///////////////////////////////////////////////////////////////////////////////
// EXECUTION
///////////////////////////////////////////////////////////////////////////////
RunTarget(target);