// Package woodpecker activates a repository in Woodpecker CI by its forge // remote id. package woodpecker import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "strconv" "strings" "time" ) // Client talks to one Woodpecker server. type Client struct { BaseURL string Token string HTTP *http.Client } // New builds a Client with a bounded HTTP client. func New(baseURL, token string) *Client { return &Client{ BaseURL: strings.TrimSuffix(baseURL, "/"), Token: token, HTTP: &http.Client{Timeout: 30 * time.Second}, } } // Repo is the subset of a Woodpecker repository record that matters here. type Repo struct { ID int64 `json:"id"` FullName string `json:"full_name"` Active bool `json:"active"` } // Enable activates the repository identified by its forge remote id. An // already-active repository answers 409, which counts as success. func (c *Client) Enable(ctx context.Context, forgeRemoteID int64) (Repo, error) { var out Repo path := "/api/repos?forge_remote_id=" + strconv.FormatInt(forgeRemoteID, 10) err := c.do(ctx, http.MethodPost, path, &out) var se *StatusError if errors.As(err, &se) && se.Status == http.StatusConflict { return out, nil } return out, err } // Lookup fetches a repository by "owner/name", used to confirm activation. func (c *Client) Lookup(ctx context.Context, fullName string) (Repo, error) { var out Repo err := c.do(ctx, http.MethodGet, "/api/repos/lookup/"+fullName, &out) return out, err } // StatusError carries a non-2xx response. type StatusError struct { Status int Path string Body string } func (e *StatusError) Error() string { msg := fmt.Sprintf("woodpecker %s: status %d", e.Path, e.Status) if e.Body != "" { msg += ": " + e.Body } return msg } func (c *Client) do(ctx context.Context, method, path string, out any) error { req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, nil) if err != nil { return err } req.Header.Set("Accept", "application/json") if c.Token != "" { req.Header.Set("Authorization", "Bearer "+c.Token) } client := c.HTTP if client == nil { client = http.DefaultClient } resp, err := client.Do(req) if err != nil { return err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode > 299 { snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) return &StatusError{Status: resp.StatusCode, Path: path, Body: strings.TrimSpace(string(snippet))} } if out == nil { _, _ = io.Copy(io.Discard, resp.Body) return nil } return json.NewDecoder(resp.Body).Decode(out) }