128 lines
5.0 KiB
C#
128 lines
5.0 KiB
C#
using ControlPlane.Core.Models;
|
|
using ControlPlane.Core.Services;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
using System.Diagnostics;
|
|
|
|
namespace ControlPlane.Api.Services;
|
|
|
|
/// <summary>
|
|
/// Runs dotnet build or npm run build for individual projects in the repo.
|
|
/// Used by the Build Monitor tab in the control plane UI.
|
|
/// </summary>
|
|
public class ProjectBuildService(
|
|
IConfiguration config,
|
|
BuildHistoryService history,
|
|
ILogger<ProjectBuildService> logger)
|
|
{
|
|
public string RepoRoot => config["Docker:RepoRoot"] ?? string.Empty;
|
|
|
|
/// <summary>Known projects in the solution, returned to the UI for the build monitor grid.</summary>
|
|
public IReadOnlyList<ProjectDefinition> GetProjects()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(RepoRoot)) return [];
|
|
|
|
return
|
|
[
|
|
new("Clarity.Server", BuildKind.DotnetProject, "Clarity.Server/Clarity.Server.csproj"),
|
|
new("Clarity.ServiceDefaults", BuildKind.DotnetProject, "Clarity.ServiceDefaults/Clarity.ServiceDefaults.csproj"),
|
|
new("frontend (Clarity.Server)", BuildKind.NpmProject, "frontend"),
|
|
];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a single project and streams output to <paramref name="onLine"/>.
|
|
/// </summary>
|
|
public async Task<BuildRecord> BuildProjectAsync(
|
|
string projectName,
|
|
Action<string> onLine,
|
|
CancellationToken ct)
|
|
{
|
|
var projects = GetProjects();
|
|
var def = projects.FirstOrDefault(p => p.Name == projectName);
|
|
if (def is null)
|
|
{
|
|
var err = new BuildRecord { Kind = BuildKind.DotnetProject, Target = projectName, Status = BuildStatus.Failed };
|
|
err.Log.Add($"Unknown project: {projectName}");
|
|
return err;
|
|
}
|
|
|
|
var record = await history.CreateBuildAsync(def.Kind, def.RelativePath);
|
|
record.Log.Add($"▶ Building {def.Name} [{def.Kind}]");
|
|
record.Log.Add($" Path: {def.RelativePath}");
|
|
record.Log.Add("──────────────────────────────────────");
|
|
onLine($"▶ Building {def.Name}");
|
|
|
|
try
|
|
{
|
|
var (exe, args, workDir) = def.Kind == BuildKind.NpmProject
|
|
? BuildNpmCommand(def.RelativePath)
|
|
: BuildDotnetCommand(def.RelativePath);
|
|
|
|
var psi = new ProcessStartInfo(exe, args)
|
|
{
|
|
WorkingDirectory = workDir,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
};
|
|
|
|
using var proc = new Process { StartInfo = psi, EnableRaisingEvents = true };
|
|
|
|
void HandleLine(string? line)
|
|
{
|
|
if (line is null) return;
|
|
record.Log.Add(line);
|
|
onLine(line);
|
|
// Non-blocking fire-and-forget flush
|
|
_ = history.AppendBuildLogAsync(record, line);
|
|
}
|
|
|
|
proc.OutputDataReceived += (_, e) => HandleLine(e.Data);
|
|
proc.ErrorDataReceived += (_, e) => HandleLine(e.Data);
|
|
|
|
proc.Start();
|
|
proc.BeginOutputReadLine();
|
|
proc.BeginErrorReadLine();
|
|
|
|
await proc.WaitForExitAsync(ct);
|
|
|
|
var status = proc.ExitCode == 0 ? BuildStatus.Succeeded : BuildStatus.Failed;
|
|
var summary = proc.ExitCode == 0 ? "✔ Build succeeded." : $"✖ Build failed (exit {proc.ExitCode}).";
|
|
onLine("──────────────────────────────────────");
|
|
onLine(summary);
|
|
record.Log.Add(summary);
|
|
|
|
await history.CompleteBuildAsync(record, status);
|
|
logger.LogInformation("Project build [{Name}] {Status}", def.Name, status);
|
|
return record;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
onLine($"✖ Exception: {ex.Message}");
|
|
record.Log.Add($"Exception: {ex.Message}");
|
|
await history.CompleteBuildAsync(record, BuildStatus.Failed);
|
|
logger.LogError(ex, "Project build [{Name}] threw.", def.Name);
|
|
return record;
|
|
}
|
|
}
|
|
|
|
private (string exe, string args, string workDir) BuildDotnetCommand(string relativePath)
|
|
{
|
|
var fullPath = Path.Combine(RepoRoot, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
|
return ("dotnet", $"build \"{fullPath}\" --configuration Release --nologo", RepoRoot);
|
|
}
|
|
|
|
private (string exe, string args, string workDir) BuildNpmCommand(string relativePath)
|
|
{
|
|
var workDir = Path.Combine(RepoRoot, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
|
// npm on Windows needs cmd /c
|
|
return (OperatingSystem.IsWindows() ? "cmd" : "sh",
|
|
OperatingSystem.IsWindows() ? "/c npm run build" : "-c \"npm run build\"",
|
|
workDir);
|
|
}
|
|
}
|
|
|
|
public record ProjectDefinition(string Name, BuildKind Kind, string RelativePath);
|