cairn/internal/forgejo/client.go

155 lines
4.1 KiB
Go

package forgejo
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type Client struct {
baseURL string
token string
httpClient *http.Client
}
func NewClient(baseURL, token string) *Client {
return &Client{
baseURL: baseURL,
token: token,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// Issue represents a Forgejo issue.
type Issue struct {
ID int64 `json:"id"`
Number int `json:"number"`
Title string `json:"title"`
Body string `json:"body"`
State string `json:"state"`
HTMLURL string `json:"html_url"`
}
// CreateIssueRequest is the body for creating a Forgejo issue.
type CreateIssueRequest struct {
Title string `json:"title"`
Body string `json:"body"`
Labels []int64 `json:"labels,omitempty"`
}
// CommitStatus represents a Forgejo commit status.
type CommitStatus struct {
State string `json:"state"`
TargetURL string `json:"target_url,omitempty"`
Description string `json:"description"`
Context string `json:"context"`
}
// CreateIssue creates a new issue on a Forgejo repository.
func (c *Client) CreateIssue(ctx context.Context, owner, repo string, req CreateIssueRequest) (*Issue, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner, repo)
var issue Issue
if err := c.post(ctx, path, req, &issue); err != nil {
return nil, fmt.Errorf("creating issue: %w", err)
}
return &issue, nil
}
// UpdateIssueState changes the state of an issue (open/closed).
func (c *Client) UpdateIssueState(ctx context.Context, owner, repo string, number int, state string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", owner, repo, number)
body := map[string]string{"state": state}
return c.patch(ctx, path, body)
}
// CreateCommitStatus posts a commit status (success/failure/pending).
func (c *Client) CreateCommitStatus(ctx context.Context, owner, repo, sha string, status CommitStatus) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s", owner, repo, sha)
return c.post(ctx, path, status, nil)
}
// CommentOnIssue adds a comment to an issue.
func (c *Client) CommentOnIssue(ctx context.Context, owner, repo string, number int, body string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, number)
return c.post(ctx, path, map[string]string{"body": body}, nil)
}
// CreateWebhook registers a webhook on a repository.
func (c *Client) CreateWebhook(ctx context.Context, owner, repo, targetURL, secret string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/hooks", owner, repo)
body := map[string]any{
"type": "forgejo",
"active": true,
"config": map[string]string{
"url": targetURL,
"content_type": "json",
"secret": secret,
},
"events": []string{"push", "issues", "pull_request"},
}
return c.post(ctx, path, body, nil)
}
func (c *Client) do(ctx context.Context, method, path string, body any) (*http.Response, error) {
var bodyReader io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
bodyReader = bytes.NewReader(data)
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bodyReader)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
if c.token != "" {
req.Header.Set("Authorization", "token "+c.token)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
respBody, _ := io.ReadAll(resp.Body)
resp.Body.Close()
return nil, fmt.Errorf("forgejo API %s %s: %d %s", method, path, resp.StatusCode, string(respBody))
}
return resp, nil
}
func (c *Client) post(ctx context.Context, path string, body any, result any) error {
resp, err := c.do(ctx, http.MethodPost, path, body)
if err != nil {
return err
}
defer resp.Body.Close()
if result != nil {
return json.NewDecoder(resp.Body).Decode(result)
}
return nil
}
func (c *Client) patch(ctx context.Context, path string, body any) error {
resp, err := c.do(ctx, http.MethodPatch, path, body)
if err != nil {
return err
}
resp.Body.Close()
return nil
}