129 lines
4.5 KiB
C#
129 lines
4.5 KiB
C#
using System.Diagnostics;
|
|
|
|
namespace w4c_workflows.Services;
|
|
|
|
/// <summary>A workflow definition file discovered in the git source.</summary>
|
|
public sealed record WorkflowFile(string Path);
|
|
|
|
/// <summary>Current git state of the workflow source (for "sync status vs git").</summary>
|
|
public sealed record WorkflowSourceState(string? HeadSha, bool Dirty);
|
|
|
|
/// <summary>
|
|
/// Reads workflow YAML definitions from the authoritative git source. v1 uses
|
|
/// the same local monorepo checkout the Source Code explorer reads
|
|
/// (<c>SourceCode:RepoRoot</c>); a Forgejo API transport can be swapped in
|
|
/// behind this interface later.
|
|
/// </summary>
|
|
public interface IWorkflowSource
|
|
{
|
|
Task<IReadOnlyList<WorkflowFile>> ListAsync(CancellationToken ct);
|
|
Task<string> ReadAsync(string path, CancellationToken ct);
|
|
WorkflowSourceState GetState();
|
|
}
|
|
|
|
public class LocalRepoWorkflowSource : IWorkflowSource
|
|
{
|
|
private const string WorkflowsDir = "workflows";
|
|
|
|
private readonly string _repoRoot;
|
|
private readonly ILogger<LocalRepoWorkflowSource> _logger;
|
|
|
|
public LocalRepoWorkflowSource(IConfiguration config, IWebHostEnvironment env, ILogger<LocalRepoWorkflowSource> logger)
|
|
{
|
|
_logger = logger;
|
|
_repoRoot = ResolveRepoRoot(config, env);
|
|
_logger.LogInformation("Workflow source repo root resolved to {Root}", _repoRoot);
|
|
}
|
|
|
|
public Task<IReadOnlyList<WorkflowFile>> ListAsync(CancellationToken ct)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
var dir = Path.Combine(_repoRoot, WorkflowsDir);
|
|
var files = new List<WorkflowFile>();
|
|
|
|
if (!Directory.Exists(dir))
|
|
return Task.FromResult<IReadOnlyList<WorkflowFile>>(files);
|
|
|
|
foreach (var full in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories))
|
|
{
|
|
var ext = Path.GetExtension(full);
|
|
if (ext is not (".yaml" or ".yml"))
|
|
continue;
|
|
var rel = Path.GetRelativePath(_repoRoot, full).Replace(Path.DirectorySeparatorChar, '/');
|
|
files.Add(new WorkflowFile(rel));
|
|
}
|
|
|
|
return Task.FromResult<IReadOnlyList<WorkflowFile>>(files.OrderBy(f => f.Path, StringComparer.Ordinal).ToList());
|
|
}
|
|
|
|
public async Task<string> ReadAsync(string path, CancellationToken ct)
|
|
=> await File.ReadAllTextAsync(Path.Combine(_repoRoot, path.Replace('/', Path.DirectorySeparatorChar)), ct);
|
|
|
|
public WorkflowSourceState GetState()
|
|
{
|
|
var head = RunGit("rev-parse", "HEAD");
|
|
var status = RunGit("status", "--porcelain");
|
|
return new WorkflowSourceState(
|
|
string.IsNullOrWhiteSpace(head) ? null : head,
|
|
!string.IsNullOrEmpty(status));
|
|
}
|
|
|
|
private string? RunGit(params string[] args)
|
|
{
|
|
try
|
|
{
|
|
var psi = new ProcessStartInfo("git")
|
|
{
|
|
WorkingDirectory = _repoRoot,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
};
|
|
foreach (var arg in args)
|
|
psi.ArgumentList.Add(arg);
|
|
|
|
using var process = Process.Start(psi);
|
|
if (process == null)
|
|
return null;
|
|
|
|
var output = process.StandardOutput.ReadToEnd();
|
|
if (!process.WaitForExit(5000))
|
|
{
|
|
process.Kill(entireProcessTree: true);
|
|
return null;
|
|
}
|
|
|
|
return process.ExitCode == 0 ? output.Trim() : null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogDebug(ex, "git invocation failed");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static string ResolveRepoRoot(IConfiguration config, IWebHostEnvironment env)
|
|
{
|
|
var configured = config["SourceCode:RepoRoot"];
|
|
if (!string.IsNullOrWhiteSpace(configured) && Directory.Exists(configured))
|
|
return Path.GetFullPath(configured);
|
|
|
|
var start = AppContext.BaseDirectory;
|
|
if (!string.IsNullOrEmpty(env.ContentRootPath) && Directory.Exists(env.ContentRootPath))
|
|
start = env.ContentRootPath;
|
|
|
|
var walk = Path.GetFullPath(start);
|
|
while (true)
|
|
{
|
|
if (Directory.Exists(Path.Combine(walk, ".git")))
|
|
return Path.GetFullPath(walk);
|
|
var parent = Directory.GetParent(walk)?.FullName;
|
|
if (string.IsNullOrEmpty(parent) || parent == walk)
|
|
break;
|
|
walk = parent;
|
|
}
|
|
|
|
return Path.GetFullPath(Directory.GetCurrentDirectory());
|
|
}
|
|
}
|