Files
unkin-agent f1bcb8cd3a
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Add the initial repospawner service
repospawner turns JSON new-repo requests into terraform-git pull requests
via kubernetes Jobs, follows those PRs to merge and optionally activates
the repository in Woodpecker.
2026-08-30 14:33:31 +10:00

103 lines
2.5 KiB
Go

// 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)
}