package agent import ( "bytes" "encoding/json" "fmt" "io" "net/http" "strings" ) // GiteaClient talks to the Gitea REST API as the agent user. type GiteaClient struct { BaseURL string Token string HTTP *http.Client } // NewGiteaClient builds a client from the configured base URL and a Vault-minted // token. func NewGiteaClient(token string) *GiteaClient { return &GiteaClient{BaseURL: GiteaURL(), Token: token, HTTP: httpClient} } func (c *GiteaClient) do(method, path string, body any, out any) error { var reader io.Reader if body != nil { b, err := json.Marshal(body) if err != nil { return err } reader = bytes.NewReader(b) } url := strings.TrimRight(c.BaseURL, "/") + path req, err := http.NewRequest(method, url, reader) if err != nil { return err } req.Header.Set("Authorization", "token "+c.Token) req.Header.Set("Accept", "application/json") if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := c.HTTP.Do(req) if err != nil { return err } defer func() { _ = resp.Body.Close() }() data, _ := io.ReadAll(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("gitea %s %s: HTTP %d: %s", method, path, resp.StatusCode, strings.TrimSpace(string(data))) } if out != nil && len(data) > 0 { if err := json.Unmarshal(data, out); err != nil { return fmt.Errorf("gitea %s %s: decoding response: %w", method, path, err) } } return nil } // User is the subset of the Gitea user object we care about. type User struct { Login string `json:"login"` ID int64 `json:"id"` } // Whoami returns the authenticated user (GET /api/v1/user). func (c *GiteaClient) Whoami() (User, error) { var u User err := c.do(http.MethodGet, "/api/v1/user", nil, &u) return u, err } // PullRequest is the subset of Gitea's PR object we track. type PullRequest struct { Number int `json:"number"` State string `json:"state"` Title string `json:"title"` Merged bool `json:"merged"` Mergeable bool `json:"mergeable"` HTMLURL string `json:"html_url"` Head struct { Sha string `json:"sha"` } `json:"head"` } // CreatePROptions are the fields for opening a PR. type CreatePROptions struct { Base string `json:"base"` Head string `json:"head"` Title string `json:"title"` Body string `json:"body"` } // CreatePR opens a pull request (POST /api/v1/repos/{owner}/{repo}/pulls). func (c *GiteaClient) CreatePR(repoPath string, opts CreatePROptions) (PullRequest, error) { var pr PullRequest err := c.do(http.MethodPost, "/api/v1/repos/"+repoPath+"/pulls", opts, &pr) return pr, err } // GetPR fetches a single pull request. func (c *GiteaClient) GetPR(repoPath string, number int) (PullRequest, error) { var pr PullRequest err := c.do(http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/pulls/%d", repoPath, number), nil, &pr) return pr, err } // Comment is the subset of an issue comment we track. type Comment struct { ID int64 `json:"id"` User User `json:"user"` Body string `json:"body"` } // CreateComment posts a comment on the PR's issue thread // (POST /api/v1/repos/{owner}/{repo}/issues/{n}/comments). func (c *GiteaClient) CreateComment(repoPath string, number int, body string) (Comment, error) { var cm Comment payload := map[string]string{"body": body} err := c.do(http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/issues/%d/comments", repoPath, number), payload, &cm) return cm, err } // ListComments lists the PR's issue comments. func (c *GiteaClient) ListComments(repoPath string, number int) ([]Comment, error) { var out []Comment err := c.do(http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/issues/%d/comments", repoPath, number), nil, &out) return out, err } // CombinedStatus is the combined commit status for a ref. type CombinedStatus struct { State string `json:"state"` } // CommitStatus returns the combined CI status for a commit SHA // (GET /api/v1/repos/{owner}/{repo}/commits/{sha}/status). An empty ref yields // an empty state without an API call. func (c *GiteaClient) CommitStatus(repoPath, sha string) (string, error) { if sha == "" { return "", nil } var cs CombinedStatus err := c.do(http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/commits/%s/status", repoPath, sha), nil, &cs) return cs.State, err }