| 1 | using System.Text.Json; |
| 2 | using AgentForge.Data; |
| 3 | using AgentForge.Data.Entities; |
| 4 | using AgentForge.Plugin.Ci.Ir; |
| 5 | using Microsoft.EntityFrameworkCore; |
| 6 | |
| 7 | namespace AgentForge.Plugin.Ci.Data; |
| 8 | |
| 9 | public sealed class ForgeCiRunStore(ForgeDbContext db) |
| 10 | { |
| 11 | public ForgeCiPipelineRunRecord? GetRun(string repoId, string runId) |
| 12 | { |
| 13 | var entity = db.CiPipelineRuns.AsNoTracking() |
| 14 | .FirstOrDefault(r => r.RepoId == repoId && r.Id == runId); |
| 15 | return entity is null ? null : ToRunRecord(entity); |
| 16 | } |
| 17 | |
| 18 | public IReadOnlyList<ForgeCiPipelineRunRecord> ListRuns(string repoId, int limit = 20) => |
| 19 | db.CiPipelineRuns.AsNoTracking() |
| 20 | .Where(r => r.RepoId == repoId) |
| 21 | .OrderByDescending(r => r.CreatedAt) |
| 22 | .Take(limit) |
| 23 | .AsEnumerable() |
| 24 | .Select(ToRunRecord) |
| 25 | .ToList(); |
| 26 | |
| 27 | public IReadOnlyList<ForgeCiStepRunRecord> ListSteps(string runId) => |
| 28 | db.CiStepRuns.AsNoTracking() |
| 29 | .Where(s => s.RunId == runId) |
| 30 | .OrderBy(s => s.OrderIndex) |
| 31 | .Select(s => ToStepRecord(s)) |
| 32 | .ToList(); |
| 33 | |
| 34 | public ForgeCiPipelineRunRecord CreateQueuedRun( |
| 35 | string repoId, |
| 36 | string definitionName, |
| 37 | string gitRef, |
| 38 | string commit, |
| 39 | int? mrNumber, |
| 40 | IReadOnlyList<ForgeCiPlanStep> plan, |
| 41 | string queuedBy) |
| 42 | { |
| 43 | var now = DateTimeOffset.UtcNow; |
| 44 | var runId = Guid.NewGuid().ToString("N"); |
| 45 | var planJson = JsonSerializer.Serialize(plan, ForgeCiDocument.JsonOptions); |
| 46 | |
| 47 | var run = new ForgeCiPipelineRunEntity |
| 48 | { |
| 49 | Id = runId, |
| 50 | RepoId = repoId, |
| 51 | DefinitionName = definitionName, |
| 52 | Ref = gitRef, |
| 53 | Commit = commit, |
| 54 | MrNumber = mrNumber, |
| 55 | Status = ForgeCiRunStatusSlugs.Pending, |
| 56 | PlanJson = planJson, |
| 57 | QueuedBy = queuedBy, |
| 58 | CreatedAt = now, |
| 59 | UpdatedAt = now, |
| 60 | }; |
| 61 | |
| 62 | var steps = new List<ForgeCiStepRunEntity>(plan.Count); |
| 63 | for (var i = 0; i < plan.Count; i++) |
| 64 | { |
| 65 | var step = plan[i]; |
| 66 | steps.Add(new ForgeCiStepRunEntity |
| 67 | { |
| 68 | Id = Guid.NewGuid().ToString("N"), |
| 69 | RunId = runId, |
| 70 | NodeId = step.NodeId, |
| 71 | TaskId = step.TaskId, |
| 72 | DisplayName = step.DisplayName, |
| 73 | OrderIndex = i, |
| 74 | Status = ForgeCiRunStatusSlugs.Pending, |
| 75 | }); |
| 76 | } |
| 77 | |
| 78 | db.CiPipelineRuns.Add(run); |
| 79 | db.CiStepRuns.AddRange(steps); |
| 80 | db.SaveChanges(); |
| 81 | return ToRunRecord(run); |
| 82 | } |
| 83 | |
| 84 | public void MarkRunRunning(string runId) |
| 85 | { |
| 86 | var run = db.CiPipelineRuns.First(r => r.Id == runId); |
| 87 | var now = DateTimeOffset.UtcNow; |
| 88 | run.Status = ForgeCiRunStatusSlugs.Running; |
| 89 | run.StartedAt ??= now; |
| 90 | run.UpdatedAt = now; |
| 91 | db.SaveChanges(); |
| 92 | } |
| 93 | |
| 94 | public void MarkRunFinished(string runId, string status) |
| 95 | { |
| 96 | var run = db.CiPipelineRuns.First(r => r.Id == runId); |
| 97 | var now = DateTimeOffset.UtcNow; |
| 98 | run.Status = status; |
| 99 | run.FinishedAt = now; |
| 100 | run.UpdatedAt = now; |
| 101 | db.SaveChanges(); |
| 102 | } |
| 103 | |
| 104 | public void MarkStepRunning(string stepId) |
| 105 | { |
| 106 | var step = db.CiStepRuns.First(s => s.Id == stepId); |
| 107 | var now = DateTimeOffset.UtcNow; |
| 108 | step.Status = ForgeCiRunStatusSlugs.Running; |
| 109 | step.StartedAt = now; |
| 110 | db.SaveChanges(); |
| 111 | } |
| 112 | |
| 113 | public void MarkStepFinished(string stepId, string status, string logText) |
| 114 | { |
| 115 | var step = db.CiStepRuns.First(s => s.Id == stepId); |
| 116 | step.Status = status; |
| 117 | step.LogText = logText; |
| 118 | step.FinishedAt = DateTimeOffset.UtcNow; |
| 119 | db.SaveChanges(); |
| 120 | |
| 121 | var run = db.CiPipelineRuns.First(r => r.Id == step.RunId); |
| 122 | run.UpdatedAt = DateTimeOffset.UtcNow; |
| 123 | db.SaveChanges(); |
| 124 | } |
| 125 | |
| 126 | public IReadOnlyList<ForgeCiPlanStep> LoadPlan(string runId) |
| 127 | { |
| 128 | var planJson = db.CiPipelineRuns.AsNoTracking().First(r => r.Id == runId).PlanJson; |
| 129 | return JsonSerializer.Deserialize<List<ForgeCiPlanStep>>(planJson, ForgeCiDocument.JsonOptions) ?? []; |
| 130 | } |
| 131 | |
| 132 | private static ForgeCiPipelineRunRecord ToRunRecord(ForgeCiPipelineRunEntity entity) => |
| 133 | new( |
| 134 | entity.Id, |
| 135 | entity.RepoId, |
| 136 | entity.DefinitionName, |
| 137 | entity.Ref, |
| 138 | entity.Commit, |
| 139 | entity.MrNumber, |
| 140 | entity.Status, |
| 141 | entity.QueuedBy, |
| 142 | entity.CreatedAt, |
| 143 | entity.UpdatedAt, |
| 144 | entity.StartedAt, |
| 145 | entity.FinishedAt); |
| 146 | |
| 147 | private static ForgeCiStepRunRecord ToStepRecord(ForgeCiStepRunEntity entity) => |
| 148 | new( |
| 149 | entity.Id, |
| 150 | entity.RunId, |
| 151 | entity.NodeId, |
| 152 | entity.TaskId, |
| 153 | entity.DisplayName, |
| 154 | entity.OrderIndex, |
| 155 | entity.Status, |
| 156 | entity.LogText, |
| 157 | entity.StartedAt, |
| 158 | entity.FinishedAt); |
| 159 | } |
| 160 | |