Files

122 lines
3.3 KiB
Go

package forge
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// State is a build's outcome, mapped onto each platform's own words.
type State string
const (
Pending State = "pending"
Success State = "success"
Failure State = "failure"
)
// Status is what a pull request shows for a commit.
type Status struct {
State State
URL string // the preview
Description string
Context string // the check's name; "hotdog-cms/preview" by default
}
// Client posts commit statuses with a platform token.
type Client struct {
Repo Repo
Token string
HTTP *http.Client
}
// Supported reports whether the platform has commit statuses at all.
func (c *Client) Supported() bool {
return c.Repo.Kind != Git && c.Repo.Kind != Gogs && c.Token != ""
}
// SetStatus posts a status on a commit. On platforms without statuses, or
// with no token, it does nothing.
func (c *Client) SetStatus(ctx context.Context, commit string, s Status) error {
if !c.Supported() {
return nil
}
if s.Context == "" {
s.Context = "hotdog-cms/preview"
}
if len(s.Description) > 140 {
s.Description = s.Description[:137] + "..."
}
var (
method = http.MethodPost
endpoint string
body any
auth string
)
r := c.Repo
switch r.Kind {
case GitHub, Gitea, Forgejo:
state := string(s.State)
endpoint = fmt.Sprintf("%s/repos/%s/statuses/%s", r.API, r.Path, url.PathEscape(commit))
body = map[string]string{"state": state, "target_url": s.URL, "description": s.Description, "context": s.Context}
auth = "Bearer " + c.Token
if r.Kind != GitHub {
auth = "token " + c.Token
}
case GitLab:
state := map[State]string{Pending: "running", Success: "success", Failure: "failed"}[s.State]
q := url.Values{"state": {state}, "name": {s.Context}, "description": {s.Description}}
if s.URL != "" {
q.Set("target_url", s.URL)
}
endpoint = fmt.Sprintf("%s/projects/%s/statuses/%s?%s", r.API, url.PathEscape(r.Path), url.PathEscape(commit), q.Encode())
auth = "PRIVATE-TOKEN " + c.Token
case Bitbucket:
state := map[State]string{Pending: "INPROGRESS", Success: "SUCCESSFUL", Failure: "FAILED"}[s.State]
endpoint = fmt.Sprintf("%s/repositories/%s/commit/%s/statuses/build", r.API, r.Path, url.PathEscape(commit))
body = map[string]string{"state": state, "key": s.Context, "name": s.Context, "url": s.URL, "description": s.Description}
auth = "Bearer " + c.Token
default:
return nil
}
var rd io.Reader
if body != nil {
b, _ := json.Marshal(body)
rd = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, endpoint, rd)
if err != nil {
return err
}
if name, value, ok := strings.Cut(auth, " "); ok && name == "PRIVATE-TOKEN" {
req.Header.Set("PRIVATE-TOKEN", value)
} else {
req.Header.Set("Authorization", auth)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "hotdog-cms")
hc := c.HTTP
if hc == nil {
hc = &http.Client{Timeout: 15 * time.Second}
}
res, err := hc.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode >= 300 {
msg, _ := io.ReadAll(io.LimitReader(res.Body, 300))
return fmt.Errorf("%s status: %s: %s", r.Kind, res.Status, strings.TrimSpace(string(msg)))
}
return nil
}