OPC # 0001: Extract OPC into standalone repo

This commit is contained in:
amadzarak
2026-04-25 17:26:42 -04:00
commit 42383bdc03
170 changed files with 21365 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using ControlPlane.Core.Models;
namespace ControlPlane.Api.Services;
/// <summary>
/// Thin wrapper around the Gitea REST API v1.
/// Configured via Gitea__BaseUrl, Gitea__Owner, and Gitea__Token in appsettings.
/// </summary>
public class GiteaService
{
private readonly HttpClient _http;
private readonly string _owner;
private readonly string _repo;
private readonly ILogger<GiteaService> _log;
private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web);
public GiteaService(IHttpClientFactory factory, IConfiguration cfg, ILogger<GiteaService> log)
{
_log = log;
_owner = cfg["Gitea:Owner"] ?? "Clarity";
_repo = cfg["Gitea:Repo"] ?? "Clarity";
var baseUrl = cfg["Gitea:BaseUrl"] ?? "https://opc.clarity.test";
var token = cfg["Gitea:Token"] ?? string.Empty;
_http = factory.CreateClient("gitea");
_http.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/api/v1/");
_http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
if (!string.IsNullOrWhiteSpace(token))
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", token);
}
// ── Repos ─────────────────────────────────────────────────────────────────
public async Task<GiteaRepo?> GetRepoAsync(CancellationToken ct = default)
{
try
{
return await _http.GetFromJsonAsync<GiteaRepo>($"repos/{_owner}/{_repo}", JsonOpts, ct);
}
catch (Exception ex) { _log.LogWarning(ex, "Gitea GetRepo failed"); return null; }
}
// ── Branches ──────────────────────────────────────────────────────────────
public async Task<List<GiteaBranch>> ListBranchesAsync(CancellationToken ct = default)
{
try
{
return await _http.GetFromJsonAsync<List<GiteaBranch>>(
$"repos/{_owner}/{_repo}/branches?limit=50", JsonOpts, ct) ?? [];
}
catch (Exception ex) { _log.LogWarning(ex, "Gitea ListBranches failed"); return []; }
}
public async Task<GiteaBranch?> CreateBranchAsync(CreateBranchRequest req, CancellationToken ct = default)
{
// Slugify: "OPC # 0032" + title → "feature/OPC-0032-git-workflow-integration"
var slug = SlugifyTitle(req.OpcTitle);
var num = req.OpcNumber.Replace("OPC # ", "OPC-").Replace(" ", "");
var branchName = $"feature/{num}-{slug}";
var body = JsonSerializer.Serialize(new
{
new_branch_name = branchName,
old_branch_name = req.From,
}, JsonOpts);
var res = await _http.PostAsync(
$"repos/{_owner}/{_repo}/branches",
new StringContent(body, Encoding.UTF8, "application/json"), ct);
if (!res.IsSuccessStatusCode)
{
var err = await res.Content.ReadAsStringAsync(ct);
_log.LogWarning("Gitea CreateBranch failed {Status}: {Error}", res.StatusCode, err);
return null;
}
return await res.Content.ReadFromJsonAsync<GiteaBranch>(JsonOpts, ct);
}
// ── Pull Requests ─────────────────────────────────────────────────────────
public async Task<List<GiteaPullRequest>> ListPullRequestsAsync(
string state = "open", CancellationToken ct = default)
{
try
{
return await _http.GetFromJsonAsync<List<GiteaPullRequest>>(
$"repos/{_owner}/{_repo}/pulls?state={state}&limit=50", JsonOpts, ct) ?? [];
}
catch (Exception ex) { _log.LogWarning(ex, "Gitea ListPRs failed"); return []; }
}
public async Task<GiteaPullRequest?> GetPullRequestAsync(long number, CancellationToken ct = default)
{
try
{
return await _http.GetFromJsonAsync<GiteaPullRequest>(
$"repos/{_owner}/{_repo}/pulls/{number}", JsonOpts, ct);
}
catch (Exception ex) { _log.LogWarning(ex, "Gitea GetPR failed"); return null; }
}
public async Task<GiteaPullRequest?> CreatePullRequestAsync(
CreatePullRequestRequest req, CancellationToken ct = default)
{
var body = JsonSerializer.Serialize(new
{
title = req.Title,
head = req.Head,
@base = req.Base,
body = req.Body,
}, JsonOpts);
var res = await _http.PostAsync(
$"repos/{_owner}/{_repo}/pulls",
new StringContent(body, Encoding.UTF8, "application/json"), ct);
if (!res.IsSuccessStatusCode)
{
var err = await res.Content.ReadAsStringAsync(ct);
_log.LogWarning("Gitea CreatePR failed {Status}: {Error}", res.StatusCode, err);
return null;
}
return await res.Content.ReadFromJsonAsync<GiteaPullRequest>(JsonOpts, ct);
}
// ── Tags ──────────────────────────────────────────────────────────────────
public async Task<List<GiteaTag>> ListTagsAsync(CancellationToken ct = default)
{
try
{
return await _http.GetFromJsonAsync<List<GiteaTag>>(
$"repos/{_owner}/{_repo}/tags?limit=20", JsonOpts, ct) ?? [];
}
catch (Exception ex) { _log.LogWarning(ex, "Gitea ListTags failed"); return []; }
}
public async Task<GiteaTag?> CreateTagAsync(CreateTagRequest req, CancellationToken ct = default)
{
var body = JsonSerializer.Serialize(new
{
tag_name = req.TagName,
message = req.Message,
target = req.CommitSha,
}, JsonOpts);
var res = await _http.PostAsync(
$"repos/{_owner}/{_repo}/tags",
new StringContent(body, Encoding.UTF8, "application/json"), ct);
if (!res.IsSuccessStatusCode)
{
var err = await res.Content.ReadAsStringAsync(ct);
_log.LogWarning("Gitea CreateTag failed {Status}: {Error}", res.StatusCode, err);
return null;
}
return await res.Content.ReadFromJsonAsync<GiteaTag>(JsonOpts, ct);
}
// ── Webhooks ──────────────────────────────────────────────────────────────
public async Task<List<GiteaWebhook>> ListWebhooksAsync(CancellationToken ct = default)
{
try
{
return await _http.GetFromJsonAsync<List<GiteaWebhook>>(
$"repos/{_owner}/{_repo}/hooks", JsonOpts, ct) ?? [];
}
catch (Exception ex) { _log.LogWarning(ex, "Gitea ListWebhooks failed"); return []; }
}
public async Task<GiteaWebhook?> RegisterWebhookAsync(
CreateWebhookRequest req, CancellationToken ct = default)
{
var body = JsonSerializer.Serialize(new
{
type = "gitea",
active = true,
config = new { url = req.TargetUrl, content_type = "json" },
events = req.Events,
}, JsonOpts);
var res = await _http.PostAsync(
$"repos/{_owner}/{_repo}/hooks",
new StringContent(body, Encoding.UTF8, "application/json"), ct);
if (!res.IsSuccessStatusCode)
{
var err = await res.Content.ReadAsStringAsync(ct);
_log.LogWarning("Gitea RegisterWebhook failed {Status}: {Error}", res.StatusCode, err);
return null;
}
return await res.Content.ReadFromJsonAsync<GiteaWebhook>(JsonOpts, ct);
}
// ── Helpers ───────────────────────────────────────────────────────────────
private static string SlugifyTitle(string title) =>
System.Text.RegularExpressions.Regex
.Replace(title.ToLowerInvariant(), @"[^a-z0-9]+", "-")
.Trim('-')[..Math.Min(40, title.Length)];
}