OPC # 0001: Gitea services

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
amadzarak
2026-04-25 19:35:46 -04:00
parent 76962a6af4
commit 65a6b4afaf
13 changed files with 457 additions and 93 deletions
+68 -24
View File
@@ -14,17 +14,19 @@ namespace ControlPlane.Api.Services;
public class GiteaService
{
private readonly HttpClient _http;
private readonly string _owner;
private readonly string _repo;
private readonly string _defaultOwner;
private readonly string _defaultRepo;
private readonly IConfiguration _cfg;
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";
_log = log;
_cfg = cfg;
_defaultOwner = cfg["Gitea:Owner"] ?? "ClarityStack";
_defaultRepo = cfg["Gitea:Repo"] ?? "OPC";
var baseUrl = cfg["Gitea:BaseUrl"] ?? "https://opc.clarity.test";
var token = cfg["Gitea:Token"] ?? string.Empty;
@@ -37,31 +39,66 @@ public class GiteaService
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", token);
}
// ── Repo resolution ───────────────────────────────────────────────────────
/// Returns (owner, repo) for the given key, falling back to the defaults.
private (string Owner, string Repo) ResolveOwnerRepo(string? repoKey = null)
{
if (!string.IsNullOrWhiteSpace(repoKey))
{
var owner = _cfg[$"Gitea:Repos:{repoKey}:Owner"];
var repo = _cfg[$"Gitea:Repos:{repoKey}:Repo"];
if (!string.IsNullOrWhiteSpace(owner) && !string.IsNullOrWhiteSpace(repo))
return (owner, repo);
}
return (_defaultOwner, _defaultRepo);
}
// ── Repos ─────────────────────────────────────────────────────────────────
public async Task<GiteaRepo?> GetRepoAsync(CancellationToken ct = default)
public async Task<GiteaRepo?> GetRepoAsync(string? repoKey = null, CancellationToken ct = default)
{
var (owner, repo) = ResolveOwnerRepo(repoKey);
try
{
return await _http.GetFromJsonAsync<GiteaRepo>($"repos/{_owner}/{_repo}", JsonOpts, ct);
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)
public async Task<List<GiteaBranch>> ListBranchesAsync(string? repoKey = null, CancellationToken ct = default)
{
var (owner, repo) = ResolveOwnerRepo(repoKey);
try
{
return await _http.GetFromJsonAsync<List<GiteaBranch>>(
$"repos/{_owner}/{_repo}/branches?limit=50", JsonOpts, ct) ?? [];
$"repos/{owner}/{repo}/branches?limit=50", JsonOpts, ct) ?? [];
}
catch (Exception ex) { _log.LogWarning(ex, "Gitea ListBranches failed"); return []; }
}
/// Returns branches from all registered repos, tagged with repoKey.
public async Task<List<(string RepoKey, GiteaBranch Branch)>> ListAllBranchesAsync(CancellationToken ct = default)
{
var result = new List<(string, GiteaBranch)>();
var section = _cfg.GetSection("Gitea:Repos");
var keys = section.GetChildren().Select(c => c.Key).ToList();
if (keys.Count == 0) keys = ["default"];
await Task.WhenAll(keys.Select(async key =>
{
var branches = await ListBranchesAsync(key == "default" ? null : key, ct);
lock (result) result.AddRange(branches.Select(b => (key, b)));
}));
return result;
}
public async Task<GiteaBranch?> CreateBranchAsync(CreateBranchRequest req, CancellationToken ct = default)
{
var (owner, repo) = ResolveOwnerRepo(req.RepoKey);
// Slugify: "OPC # 0032" + title → "feature/OPC-0032-git-workflow-integration"
var slug = SlugifyTitle(req.OpcTitle);
var num = req.OpcNumber.Replace("OPC # ", "OPC-").Replace(" ", "");
@@ -74,7 +111,7 @@ public class GiteaService
}, JsonOpts);
var res = await _http.PostAsync(
$"repos/{_owner}/{_repo}/branches",
$"repos/{owner}/{repo}/branches",
new StringContent(body, Encoding.UTF8, "application/json"), ct);
if (!res.IsSuccessStatusCode)
@@ -90,29 +127,32 @@ public class GiteaService
// ── Pull Requests ─────────────────────────────────────────────────────────
public async Task<List<GiteaPullRequest>> ListPullRequestsAsync(
string state = "open", CancellationToken ct = default)
string state = "open", string? repoKey = null, CancellationToken ct = default)
{
var (owner, repo) = ResolveOwnerRepo(repoKey);
try
{
return await _http.GetFromJsonAsync<List<GiteaPullRequest>>(
$"repos/{_owner}/{_repo}/pulls?state={state}&limit=50", JsonOpts, ct) ?? [];
$"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)
public async Task<GiteaPullRequest?> GetPullRequestAsync(long number, string? repoKey = null, CancellationToken ct = default)
{
var (owner, repo) = ResolveOwnerRepo(repoKey);
try
{
return await _http.GetFromJsonAsync<GiteaPullRequest>(
$"repos/{_owner}/{_repo}/pulls/{number}", JsonOpts, ct);
$"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)
CreatePullRequestRequest req, string? repoKey = null, CancellationToken ct = default)
{
var (owner, repo) = ResolveOwnerRepo(repoKey);
var body = JsonSerializer.Serialize(new
{
title = req.Title,
@@ -122,7 +162,7 @@ public class GiteaService
}, JsonOpts);
var res = await _http.PostAsync(
$"repos/{_owner}/{_repo}/pulls",
$"repos/{owner}/{repo}/pulls",
new StringContent(body, Encoding.UTF8, "application/json"), ct);
if (!res.IsSuccessStatusCode)
@@ -137,18 +177,20 @@ public class GiteaService
// ── Tags ──────────────────────────────────────────────────────────────────
public async Task<List<GiteaTag>> ListTagsAsync(CancellationToken ct = default)
public async Task<List<GiteaTag>> ListTagsAsync(string? repoKey = null, CancellationToken ct = default)
{
var (owner, repo) = ResolveOwnerRepo(repoKey);
try
{
return await _http.GetFromJsonAsync<List<GiteaTag>>(
$"repos/{_owner}/{_repo}/tags?limit=20", JsonOpts, ct) ?? [];
$"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)
public async Task<GiteaTag?> CreateTagAsync(CreateTagRequest req, string? repoKey = null, CancellationToken ct = default)
{
var (owner, repo) = ResolveOwnerRepo(repoKey);
var body = JsonSerializer.Serialize(new
{
tag_name = req.TagName,
@@ -157,7 +199,7 @@ public class GiteaService
}, JsonOpts);
var res = await _http.PostAsync(
$"repos/{_owner}/{_repo}/tags",
$"repos/{owner}/{repo}/tags",
new StringContent(body, Encoding.UTF8, "application/json"), ct);
if (!res.IsSuccessStatusCode)
@@ -172,19 +214,21 @@ public class GiteaService
// ── Webhooks ──────────────────────────────────────────────────────────────
public async Task<List<GiteaWebhook>> ListWebhooksAsync(CancellationToken ct = default)
public async Task<List<GiteaWebhook>> ListWebhooksAsync(string? repoKey = null, CancellationToken ct = default)
{
var (owner, repo) = ResolveOwnerRepo(repoKey);
try
{
return await _http.GetFromJsonAsync<List<GiteaWebhook>>(
$"repos/{_owner}/{_repo}/hooks", JsonOpts, ct) ?? [];
$"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)
CreateWebhookRequest req, string? repoKey = null, CancellationToken ct = default)
{
var (owner, repo) = ResolveOwnerRepo(repoKey);
var body = JsonSerializer.Serialize(new
{
type = "gitea",
@@ -194,7 +238,7 @@ public class GiteaService
}, JsonOpts);
var res = await _http.PostAsync(
$"repos/{_owner}/{_repo}/hooks",
$"repos/{owner}/{repo}/hooks",
new StringContent(body, Encoding.UTF8, "application/json"), ct);
if (!res.IsSuccessStatusCode)