Forge
csharp3407750f
1using AgentForge.Plugin.Sdk;
2using AgentForge.Abstractions;
3using AgentForge.Contracts;
4using AgentForge.Data;
5using AgentForge.Models;
6using AgentForge.Services;
7
8namespace AgentForge.Plugin.Issue.Endpoints;
9
10internal static class ForgeIssueEndpoints
11{
12 internal static RouteGroupBuilder MapIssueEndpoints(this RouteGroupBuilder api)
13 {
14 api.MapGet($"/repos/{ForgeRepoRouting.Pattern}/issues", ListIssues);
15 api.MapPost($"/repos/{ForgeRepoRouting.Pattern}/issues", CreateIssue);
16 api.MapGet($"/repos/{ForgeRepoRouting.Pattern}/issues/{{number:int}}", GetIssue);
17 api.MapPatch($"/repos/{ForgeRepoRouting.Pattern}/issues/{{number:int}}", UpdateIssue);
18 api.MapPost($"/repos/{ForgeRepoRouting.Pattern}/issues/{{number:int}}/comments", AddIssueComment);
19 api.MapPost($"/repos/{ForgeRepoRouting.Pattern}/issues/{{number:int}}/anchors", LinkIssueAnchors);
20 return api;
21 }
22
23 private static IResult ListIssues(
24 string name,
25 string? q,
26 HttpContext http,
27 ForgeRepository repository,
28 ForgeUrls urls)
29 {
30 var repo = ForgePluginEndpoints.RequireRepo(repository, name, http);
31 var issues = string.IsNullOrWhiteSpace(q)
32 ? repository.ListIssues(repo.Id)
33 : repository.SearchIssues(repo.Id, q);
34 var responses = issues.Select(issue => ForgePluginResponses.ToIssueResponse(issue, repo.Name, urls)).ToList();
35 return Results.Ok(new IssueListResponse(responses.Count, responses));
36 }
37
38 private static async Task<IResult> CreateIssue(
39 string name,
40 CreateIssueRequest request,
41 HttpContext context,
42 ForgeRepository repository,
43 ForgeUrls urls,
44 CiWebhookService ci,
45 CancellationToken cancellationToken)
46 {
47 var repo = ForgePluginEndpoints.RequireRepo(repository, name, context);
48 var title = request.Title?.Trim() ?? string.Empty;
49 if (title.Length == 0)
50 throw new ForgeApiException(400, "Issue title is required.");
51
52 var anchors = IssueAnchorResolver.Resolve(request.Anchors, request.AnchorBrackets);
53 var createdBy = ForgeActorResolver.ResolveActor(context, request.Author);
54 var issue = repository.InsertIssue(
55 repo.Id,
56 title,
57 request.Body?.Trim() ?? string.Empty,
58 anchors,
59 createdBy);
60 var response = ForgePluginResponses.ToIssueResponse(issue, repo.Name, urls);
61 await ci.EmitIopAsync(
62 ForgeIopEvent.ForIssue("issue.opened", repo.Name, issue.Number, response, createdBy, issue.Anchors),
63 cancellationToken).ConfigureAwait(false);
64 return Results.Created($"/api/v1/repos/{name}/issues/{issue.Number}", response);
65 }
66
67 private static IResult GetIssue(
68 string name,
69 int number,
70 HttpContext http,
71 ForgeRepository repository,
72 ForgeUrls urls)
73 {
74 var repo = ForgePluginEndpoints.RequireRepo(repository, name, http);
75 var issue = repository.GetIssueByNumber(repo.Id, number)
76 ?? throw new ForgeApiException(404, $"Issue #{number} not found.");
77 var comments = repository.ListIssueComments(issue.Id)
78 .Select(ForgePluginResponses.ToCommentResponse)
79 .ToList();
80 return Results.Ok(ForgePluginResponses.ToIssueDetailResponse(issue, repo.Name, urls, comments));
81 }
82
83 private static IResult UpdateIssue(
84 string name,
85 int number,
86 UpdateIssueRequest request,
87 HttpContext http,
88 ForgeRepository repository,
89 ForgeUrls urls)
90 {
91 var repo = ForgePluginEndpoints.RequireRepo(repository, name, http);
92 if (request.Title is null && request.Body is null && request.Status is null)
93 throw new ForgeApiException(400, "At least one of title, body, or status is required.");
94
95 var issue = repository.UpdateIssue(repo.Id, number, request);
96 return Results.Ok(ForgePluginResponses.ToIssueResponse(issue, repo.Name, urls));
97 }
98
99 private static async Task<IResult> AddIssueComment(
100 string name,
101 int number,
102 AddIssueCommentRequest request,
103 HttpContext context,
104 ForgeRepository repository,
105 CiWebhookService ci,
106 CancellationToken cancellationToken)
107 {
108 var repo = ForgePluginEndpoints.RequireRepo(repository, name, context);
109 var body = request.Body?.Trim() ?? string.Empty;
110 if (body.Length == 0)
111 throw new ForgeApiException(400, "Comment body is required.");
112
113 var author = ForgeActorResolver.ResolveCommentAuthor(context, request.Author);
114 var comment = repository.InsertIssueComment(repo.Id, number, body, author);
115 var response = ForgePluginResponses.ToCommentResponse(comment);
116 await ci.EmitIopAsync(
117 ForgeIopEvent.ForIssue("issue.comment", repo.Name, number, response, author),
118 cancellationToken).ConfigureAwait(false);
119 return Results.Created(
120 $"/api/v1/repos/{name}/issues/{number}/comments/{comment.Id}",
121 response);
122 }
123
124 private static IResult LinkIssueAnchors(
125 string name,
126 int number,
127 LinkIssueAnchorsRequest request,
128 HttpContext http,
129 ForgeRepository repository,
130 ForgeUrls urls)
131 {
132 var repo = ForgePluginEndpoints.RequireRepo(repository, name, http);
133 var anchors = IssueAnchorResolver.Resolve(request.Anchors, request.AnchorBrackets);
134 if (anchors is null || anchors.Count == 0)
135 throw new ForgeApiException(400, "At least one anchor (anchors or anchorBrackets) is required.");
136
137 var issue = repository.LinkIssueAnchors(repo.Id, number, anchors);
138 return Results.Ok(ForgePluginResponses.ToIssueResponse(issue, repo.Name, urls));
139 }
140}
141
View only · write via MCP/CIDE