OPC # 0001: Extract OPC into standalone repo
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user