feat. rebuild service in Go
ci / test (pull_request) Successful in 1m54s
ci / publish (pull_request) Has been skipped

Rebuild the Gitea Codex review bot from the product contract with a Go HTTP service, durable SQL queue, typed Gitea client, isolated runner, and deployment updates.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Space-Banane
2026-07-12 21:51:01 +02:00
parent fdd3819ff8
commit f19b271642
74 changed files with 2623 additions and 4819 deletions
+196
View File
@@ -0,0 +1,196 @@
package gitea
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"gitea-codex-bot/internal/config"
"gitea-codex-bot/internal/domain"
)
type Client struct {
baseURL, token string
httpClient *http.Client
}
func NewClient(settings config.Settings) *Client {
return &Client{baseURL: strings.TrimRight(settings.GiteaBaseURL, "/"), token: settings.GiteaToken, httpClient: &http.Client{Timeout: 20 * time.Second}}
}
func (c *Client) request(ctx context.Context, method, path string, body any) ([]byte, int, error) {
var reader io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return nil, 0, err
}
reader = strings.NewReader(string(data))
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, reader)
if err != nil {
return nil, 0, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "token "+c.token)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
data, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
if err != nil {
return nil, resp.StatusCode, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return data, resp.StatusCode, fmt.Errorf("gitea returned HTTP %d", resp.StatusCode)
}
return data, resp.StatusCode, nil
}
func splitRepo(repo string) (string, string, error) {
owner, name, ok := strings.Cut(repo, "/")
if !ok || owner == "" || name == "" {
return "", "", fmt.Errorf("invalid repository %q", repo)
}
return url.PathEscape(owner), url.PathEscape(name), nil
}
func (c *Client) GetPullRequest(ctx context.Context, repo string, number int) (domain.PullRequestContext, error) {
owner, name, err := splitRepo(repo)
if err != nil {
return domain.PullRequestContext{}, err
}
data, _, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner, name, number), nil)
if err != nil {
return domain.PullRequestContext{}, err
}
var p struct {
HTMLURL string `json:"html_url"`
Base struct {
Ref, SHA string
Repo struct {
CloneURL string `json:"clone_url"`
FullName string `json:"full_name"`
} `json:"repo"`
} `json:"base"`
Head struct {
Ref, SHA string
Repo struct {
CloneURL string `json:"clone_url"`
FullName string `json:"full_name"`
} `json:"repo"`
} `json:"head"`
}
if err := json.Unmarshal(data, &p); err != nil {
return domain.PullRequestContext{}, err
}
if p.Base.SHA == "" || p.Head.SHA == "" || p.Base.Repo.CloneURL == "" || p.Head.Repo.CloneURL == "" {
return domain.PullRequestContext{}, fmt.Errorf("gitea pull request response missing required fields")
}
return domain.PullRequestContext{Repo: repo, PRNumber: number, BaseRef: p.Base.Ref, BaseSHA: p.Base.SHA, HeadRef: p.Head.Ref, HeadSHA: p.Head.SHA, CloneURL: p.Head.Repo.CloneURL, BaseCloneURL: p.Base.Repo.CloneURL, HeadCloneURL: p.Head.Repo.CloneURL, HTMLURL: p.HTMLURL, IsFork: p.Base.Repo.FullName != p.Head.Repo.FullName}, nil
}
func (c *Client) GetFileContent(ctx context.Context, repo, path, ref string) (string, bool, error) {
owner, name, err := splitRepo(repo)
if err != nil {
return "", false, err
}
data, status, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?ref=%s", owner, name, url.PathEscape(path), url.QueryEscape(ref)), nil)
if err != nil && status == http.StatusNotFound {
return "", false, nil
}
if err != nil {
return "", false, err
}
var p struct {
Content string `json:"content"`
Encoding string `json:"encoding"`
}
if err := json.Unmarshal(data, &p); err != nil {
return "", false, err
}
if p.Encoding != "base64" || p.Content == "" {
return "", false, nil
}
decoded, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(p.Content, "\n", ""))
if err != nil {
return "", false, err
}
return string(decoded), true, nil
}
func (c *Client) PostIssueComment(ctx context.Context, repo string, number int, body string) (int64, error) {
owner, name, err := splitRepo(repo)
if err != nil {
return 0, err
}
data, _, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, name, number), map[string]string{"body": body})
if err != nil {
return 0, err
}
var p struct {
ID int64 `json:"id"`
}
if err := json.Unmarshal(data, &p); err != nil {
return 0, err
}
return p.ID, nil
}
func (c *Client) EditIssueComment(ctx context.Context, repo string, commentID int64, body string) (int64, error) {
owner, name, err := splitRepo(repo)
if err != nil {
return 0, err
}
data, _, err := c.request(ctx, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d", owner, name, commentID), map[string]string{"body": body})
if err != nil {
return 0, err
}
var p struct {
ID int64 `json:"id"`
}
if err := json.Unmarshal(data, &p); err != nil {
return 0, err
}
return p.ID, nil
}
func (c *Client) GetIssueComments(ctx context.Context, repo string, number int) ([]map[string]any, error) {
owner, name, err := splitRepo(repo)
if err != nil {
return nil, err
}
data, _, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, name, number), nil)
if err != nil {
return nil, err
}
var p []map[string]any
if err := json.Unmarshal(data, &p); err != nil {
return nil, err
}
return p, nil
}
func (c *Client) GetIssueComment(ctx context.Context, repo string, commentID int64) (map[string]any, error) {
owner, name, err := splitRepo(repo)
if err != nil {
return nil, err
}
data, _, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d", owner, name, commentID), nil)
if err != nil {
return nil, err
}
var p map[string]any
if err := json.Unmarshal(data, &p); err != nil {
return nil, err
}
return p, nil
}
var _ = strconv.Itoa