release/github.go

306 lines
7.4 KiB
Go
Raw Normal View History

2024-10-30 21:28:34 +00:00
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"os"
"strings"
)
type ApiClient interface {
Do(*http.Request) (*http.Response, error)
}
type GitHub struct {
client ApiClient
}
type ApiError struct {
Message string `json:"message"`
URL string `json:"url"`
}
func (e ApiError) Error() string {
return e.Message
}
type ApiNotFoundError struct {
ApiError
Errors []string `json:"errors"`
}
type ApiValidationError struct {
ApiError
}
func NewGitHub(client ApiClient) *GitHub {
return &GitHub{client: client}
}
// Determine whether the request `content-type` includes a
// server-acceptable mime-type
//
// Failure should yield an HTTP 415 (`http.StatusUnsupportedMediaType`)
func HasContentType(r *http.Response, mimetype string) bool {
contentType := r.Header.Get("Content-type")
if contentType == "" {
return mimetype == "application/octet-stream"
}
for _, v := range strings.Split(contentType, ",") {
t, _, err := mime.ParseMediaType(v)
if err != nil {
break
}
if t == mimetype {
return true
}
}
return false
}
type GitTag struct {
Commit struct {
Created string `json:"created,omitempty"`
SHA string `json:"sha,omitempty"`
URL string `json:"url,omitempty"`
} `json:"commit"`
Id string `json:"id,omitempty"`
Message string `json:"message,omitempty"`
Name string `json:"name,omitempty"`
TarballURL string `json:"tarball_url,omitempty"`
ZipballURL string `json:"zipball_url,omitempty"`
}
func (gh *GitHub) GetGitTagInfo(owner, repo, tag string, ctx context.Context) (*GitTag, error) {
url := fmt.Sprintf("/repos/%s/%s/tags/%s", owner, repo, tag)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("accept", "application/json")
res, err := gh.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if !HasContentType(res, "application/json") {
return nil, fmt.Errorf("content-type: %q should be %q",
res.Header.Get("content-type"), "application/json")
}
dec := json.NewDecoder(io.LimitReader(res.Body, 32*1024))
if res.StatusCode >= 200 && res.StatusCode < 300 {
var cres GitTag
err := dec.Decode(&cres)
return &cres, err
} else {
switch res.StatusCode {
case 404:
var error ApiNotFoundError
err := dec.Decode(&error)
if err != nil {
return nil, err
}
return nil, error
default:
return nil, fmt.Errorf("an unexpected error occurred")
}
}
}
type CreateReleaseRequest struct {
Owner string `json:"-"`
Repo string `json:"-"`
Body string `json:"body,omitempty"`
Draft bool `json:"draft,omitempty"`
HideArchiveLinks bool `json:"hide_archive_links,omitempty"`
Name string `json:"name,omitempty"`
PreRelease bool `json:"prerelease,omitempty"`
TagName string `json:"tag_name"`
TargetCommitish string `json:"target_commitish,omitempty"`
}
type CreateReleaseResponse struct {
Body string `json:"body"`
CreatedAt string `json:"created_at"`
Draft bool `json:"draft"`
HideArchiveLinks bool `json:"hide_archive_links"`
HtmlURL string `json:"html_url"`
Id int64 `json:"id"`
Name string `json:"name"`
PreRelease bool `json:"prerelease"`
PublishedAt string `json:"published_at"`
TagName string `json:"tag_name"`
TarballURL string `json:"tarball_url"`
TargetCommitish string `json:"target_commitish"`
UploadURL string `json:"upload_url"`
URL string `json:"url"`
ZipballURL string `json:"zipball_url"`
}
func (gh *GitHub) CreateRelease(creq *CreateReleaseRequest, ctx context.Context) (*CreateReleaseResponse, error) {
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(creq); err != nil {
return nil, err
}
url := fmt.Sprintf("/repos/%s/%s/releases", creq.Owner, creq.Repo)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &b)
if err != nil {
return nil, err
}
req.Header.Set("content-type", "application/json")
req.Header.Set("accept", "application/json")
res, err := gh.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if !HasContentType(res, "application/json") {
return nil, fmt.Errorf("content-type: %q should be %q",
res.Header.Get("content-type"), "application/json")
}
dec := json.NewDecoder(io.LimitReader(res.Body, 32*1024))
if res.StatusCode >= 200 && res.StatusCode < 300 {
var cres CreateReleaseResponse
err := dec.Decode(&cres)
return &cres, err
} else {
switch res.StatusCode {
case 404:
var error ApiNotFoundError
err := dec.Decode(&error)
if err != nil {
return nil, err
}
return nil, error
case 409:
var error ApiError
err := dec.Decode(&error)
if err != nil {
return nil, err
}
return nil, error
case 422:
var error ApiValidationError
err := dec.Decode(&error)
if err != nil {
return nil, err
}
return nil, error
default:
return nil, fmt.Errorf("an unexpected error occurred")
}
}
}
type CreateReleaseArtifactRequest struct {
Owner string `json:"-"`
Repo string `json:"-"`
Id int64 `json:"-"`
Name string `json:"-"`
ExternalUrl string `json:"-"`
Attachment io.Reader `json:"-"`
}
type CreateReleaseArtifactResponse struct {
BrowserDownloadUrl string `json:"browser_download_url"`
CreatedAt string `json:"created_at"`
DownloadCount int64 `json:"download_count"`
Id int64 `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
Type string `json:"type"`
Uuid string `json:"uuid"`
}
func (gh *GitHub) CreateReleaseArtifact(creq *CreateReleaseArtifactRequest, ctx context.Context) (*CreateReleaseArtifactResponse, error) {
var b bytes.Buffer
w := multipart.NewWriter(&b)
if c, ok := creq.Attachment.(io.Closer); ok {
defer c.Close()
}
var fw io.Writer
var rd io.Reader
var err error
if f, ok := creq.Attachment.(*os.File); ok {
fw, err = w.CreateFormFile("attachment", f.Name())
if err != nil {
return nil, err
}
rd = f
} else {
fw, err = w.CreateFormField("external_url")
if err != nil {
return nil, err
}
rd = bytes.NewBufferString(creq.ExternalUrl)
}
if _, err := io.Copy(fw, rd); err != nil {
return nil, err
}
w.Close()
url := fmt.Sprintf("/repos/%s/%s/releases/%d/assets", creq.Owner, creq.Repo, creq.Id)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &b)
if err != nil {
return nil, err
}
req.Header.Set("content-type", w.FormDataContentType())
req.Header.Set("accept", "application/json")
res, err := gh.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if !HasContentType(res, "application/json") {
return nil, fmt.Errorf("content-type: %q should be %q",
res.Header.Get("content-type"), "application/json")
}
dec := json.NewDecoder(io.LimitReader(res.Body, 32*1024))
if res.StatusCode >= 200 && res.StatusCode < 300 {
var cres CreateReleaseArtifactResponse
err := dec.Decode(&cres)
return &cres, err
} else {
switch res.StatusCode {
case 400:
var error ApiError
err := dec.Decode(&error)
if err != nil {
return nil, err
}
return nil, error
case 404:
var error ApiNotFoundError
err := dec.Decode(&error)
if err != nil {
return nil, err
}
return nil, error
case 413:
fallthrough // quota?
default:
return nil, fmt.Errorf("an unexpected error occurred")
}
}
}