Forge
csharp3407750f
1using AgentForge.Abstractions;
2using AgentForge.Contracts;
3using AgentForge.Data;
4using AgentForge.Plugin.Ci.Contracts;
5using AgentForge.Plugin.Ci.Data;
6using AgentForge.Plugin.Ci.Execution;
7using AgentForge.Plugin.Ci.Ir;
8using AgentForge.Plugin.Sdk;
9using AgentForge.Plugin.View;
10using AgentForge.Services;
11
12namespace AgentForge.Plugin.Ci.Endpoints;
13
14internal static class ForgeCiEndpoints
15{
16 internal static RouteGroupBuilder MapCiEndpoints(this RouteGroupBuilder api)
17 {
18 api.MapGet($"/repos/{ForgeRepoRouting.Pattern}/ci/definitions", ListDefinitions);
19 api.MapPost($"/repos/{ForgeRepoRouting.Pattern}/ci/definitions", CreateDefinition);
20 api.MapGet($"/repos/{ForgeRepoRouting.Pattern}/ci/definitions/{{definitionName}}", GetDefinition);
21 api.MapPut($"/repos/{ForgeRepoRouting.Pattern}/ci/definitions/{{definitionName}}", UpdateDefinition);
22 api.MapGet($"/repos/{ForgeRepoRouting.Pattern}/ci/tasks", GetTaskCatalog);
23 api.MapGet($"/repos/{ForgeRepoRouting.Pattern}/ci/runs", ListRuns);
24 api.MapPost($"/repos/{ForgeRepoRouting.Pattern}/ci/runs", QueueRun);
25 api.MapGet($"/repos/{ForgeRepoRouting.Pattern}/ci/runs/{{runId}}", GetRun);
26 return api;
27 }
28
29 private static IResult ListDefinitions(
30 string name,
31 HttpContext http,
32 ForgeRepository repository,
33 ForgeCiDefinitionStore definitions)
34 {
35 var repo = ForgePluginEndpoints.RequireRepo(repository, name, http);
36 var rows = definitions.List(repo.Id);
37 var items = rows.Select(r => ToSummary(r, repo.Name, definitions)).ToList();
38 return Results.Ok(new CiDefinitionListResponse(items.Count, items));
39 }
40
41 private static IResult CreateDefinition(
42 string name,
43 CreateCiDefinitionRequest request,
44 HttpContext context,
45 ForgeRepository repository,
46 ForgeCiDefinitionStore definitions)
47 {
48 var repo = ForgePluginEndpoints.RequireRepo(repository, name, context);
49 var definitionName = request.Name?.Trim();
50 if (string.IsNullOrEmpty(definitionName))
51 throw new ForgeApiException(400, "Definition name is required.");
52
53 ForgeCiDocument document;
54 try
55 {
56 document = request.Document is null
57 ? ForgeCiTemplates.CreateDefault(definitionName)
58 : request.Document;
59 document.Name = ForgeCiTemplates.NormalizeDefinitionName(definitionName);
60 }
61 catch (ForgeCiValidationException ex)
62 {
63 throw new ForgeApiException(400, ex.Message);
64 }
65
66 var createdBy = ForgeActorResolver.ResolveActor(context, null);
67 try
68 {
69 var record = definitions.Insert(repo.Id, document, createdBy);
70 return Results.Created(
71 $"/api/v1/repos/{ForgeRepoNames.ToUrlPath(repo.Name)}/ci/definitions/{Uri.EscapeDataString(record.Name)}",
72 ToDetail(record, repo.Name, definitions));
73 }
74 catch (InvalidOperationException ex)
75 {
76 throw new ForgeApiException(409, ex.Message);
77 }
78 catch (ForgeCiValidationException ex)
79 {
80 throw new ForgeApiException(400, ex.Message);
81 }
82 }
83
84 private static IResult GetDefinition(
85 string name,
86 string definitionName,
87 HttpContext http,
88 ForgeRepository repository,
89 ForgeCiDefinitionStore definitions)
90 {
91 var repo = ForgePluginEndpoints.RequireRepo(repository, name, http);
92 var normalized = ForgeCiTemplates.NormalizeDefinitionName(definitionName);
93 var record = definitions.GetByName(repo.Id, normalized)
94 ?? throw new ForgeApiException(404, $"CI definition '{normalized}' not found.");
95 return Results.Ok(ToDetail(record, repo.Name, definitions));
96 }
97
98 private static IResult UpdateDefinition(
99 string name,
100 string definitionName,
101 UpdateCiDefinitionRequest request,
102 HttpContext http,
103 ForgeRepository repository,
104 ForgeCiDefinitionStore definitions)
105 {
106 var repo = ForgePluginEndpoints.RequireRepo(repository, name, http);
107 var normalized = ForgeCiTemplates.NormalizeDefinitionName(definitionName);
108 try
109 {
110 request.Document.Name = normalized;
111 var record = definitions.Update(repo.Id, normalized, request.Document);
112 return Results.Ok(ToDetail(record, repo.Name, definitions));
113 }
114 catch (KeyNotFoundException)
115 {
116 throw new ForgeApiException(404, $"CI definition '{normalized}' not found.");
117 }
118 catch (ForgeCiValidationException ex)
119 {
120 throw new ForgeApiException(400, ex.Message);
121 }
122 }
123
124 private static IResult GetTaskCatalog()
125 {
126 var tasks = ForgeCiTaskCatalog.All
127 .Select(t => new CiTaskDescriptorResponse(t.Id, t.DisplayName, t.Description))
128 .ToList();
129 return Results.Ok(new CiTaskCatalogResponse(tasks));
130 }
131
132 private static IResult ListRuns(
133 string name,
134 HttpContext http,
135 ForgeRepository repository,
136 ForgeCiRunStore runs)
137 {
138 var repo = ForgePluginEndpoints.RequireRepo(repository, name, http);
139 var items = ForgeCiRunResolve.ListSummaries(repo, runs);
140 return Results.Ok(new CiRunListResponse(items.Count, items));
141 }
142
143 private static async Task<IResult> QueueRun(
144 string name,
145 QueueCiRunRequest request,
146 HttpContext context,
147 ForgeRepository repository,
148 ForgeCiRunQueueService queue,
149 CancellationToken cancellationToken)
150 {
151 var repo = ForgePluginEndpoints.RequireRepo(repository, name, context);
152 if (string.IsNullOrWhiteSpace(request.DefinitionName))
153 throw new ForgeApiException(400, "Definition name is required.");
154
155 var queuedBy = ForgeActorResolver.ResolveActor(context, null);
156 if (string.IsNullOrWhiteSpace(queuedBy))
157 queuedBy = "api";
158 try
159 {
160 var detail = await queue.QueueAsync(
161 repo.Id,
162 repo.Name,
163 request,
164 queuedBy,
165 cancellationToken).ConfigureAwait(false);
166 return Results.Accepted(
167 $"/api/v1/repos/{ForgeRepoNames.ToUrlPath(repo.Name)}/ci/runs/{Uri.EscapeDataString(detail.Id)}",
168 detail);
169 }
170 catch (KeyNotFoundException ex)
171 {
172 throw new ForgeApiException(404, ex.Message);
173 }
174 catch (ForgeCiValidationException ex)
175 {
176 throw new ForgeApiException(400, ex.Message);
177 }
178 }
179
180 private static IResult GetRun(
181 string name,
182 string runId,
183 HttpContext http,
184 ForgeRepository repository,
185 ForgeCiRunStore runs)
186 {
187 var repo = ForgePluginEndpoints.RequireRepo(repository, name, http);
188 var runScope = ForgeCiRunResolve.TryGetRunScope(repo, runs, runId);
189 if (runScope is null)
190 throw new ForgeApiException(404, $"CI run '{runId}' not found.");
191
192 var steps = runScope.Runs.ListSteps(runScope.Run.Id);
193 return Results.Ok(ForgeCiRunQueueService.ToDetail(repo.Name, runScope.Run, steps));
194 }
195
196 private static CiDefinitionSummaryResponse ToSummary(
197 ForgeCiDefinitionRecord record,
198 string repoName,
199 ForgeCiDefinitionStore store)
200 {
201 var document = store.LoadDocument(record);
202 var taskCount = document.Nodes.Count(n => n.Kind == "task");
203 return new CiDefinitionSummaryResponse(
204 record.Name,
205 repoName,
206 record.CreatedBy,
207 record.CreatedAt,
208 record.UpdatedAt,
209 taskCount);
210 }
211
212 private static CiDefinitionDetailResponse ToDetail(
213 ForgeCiDefinitionRecord record,
214 string repoName,
215 ForgeCiDefinitionStore store)
216 {
217 var document = store.LoadDocument(record);
218 return new CiDefinitionDetailResponse(
219 record.Name,
220 repoName,
221 record.CreatedBy,
222 record.CreatedAt,
223 record.UpdatedAt,
224 document);
225 }
226}
227
View only · write via MCP/CIDE