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