cli/pkg/cmd/release/shared/fetch.go
Mislav Marohnić c667a0bc49 Fix fetching draft releases from GitHub Actions
When using GITHUB_TOKEN in Actions, the permissions on a repository are
null and therefore we can't check whether the viewer has push access or
not. The solution is to unconditionally check for draft releases instead
of trying to be smart about it. Draft releases are going to be on top,
so we don't have to paginate through all releases in a repository.
2021-05-18 19:44:29 +02:00

234 lines
5.3 KiB
Go

package shared
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"time"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/internal/ghrepo"
)
var ReleaseFields = []string{
"url",
"apiUrl",
"uploadUrl",
"tarballUrl",
"zipballUrl",
"id",
"tagName",
"name",
"body",
"isDraft",
"isPrerelease",
"createdAt",
"publishedAt",
"targetCommitish",
"author",
"assets",
}
type Release struct {
ID string `json:"node_id"`
TagName string `json:"tag_name"`
Name string `json:"name"`
Body string `json:"body"`
IsDraft bool `json:"draft"`
IsPrerelease bool `json:"prerelease"`
CreatedAt time.Time `json:"created_at"`
PublishedAt *time.Time `json:"published_at"`
TargetCommitish string `json:"target_commitish"`
APIURL string `json:"url"`
UploadURL string `json:"upload_url"`
TarballURL string `json:"tarball_url"`
ZipballURL string `json:"zipball_url"`
URL string `json:"html_url"`
Assets []ReleaseAsset
Author struct {
ID string `json:"node_id"`
Login string `json:"login"`
}
}
type ReleaseAsset struct {
ID string `json:"node_id"`
Name string
Label string
Size int64
State string
APIURL string `json:"url"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DownloadCount int `json:"download_count"`
ContentType string `json:"content_type"`
BrowserDownloadURL string `json:"browser_download_url"`
}
func (rel *Release) ExportData(fields []string) *map[string]interface{} {
v := reflect.ValueOf(rel).Elem()
fieldByName := func(v reflect.Value, field string) reflect.Value {
return v.FieldByNameFunc(func(s string) bool {
return strings.EqualFold(field, s)
})
}
data := map[string]interface{}{}
for _, f := range fields {
switch f {
case "author":
data[f] = map[string]interface{}{
"id": rel.Author.ID,
"login": rel.Author.Login,
}
case "assets":
assets := make([]interface{}, 0, len(rel.Assets))
for _, a := range rel.Assets {
assets = append(assets, map[string]interface{}{
"url": a.BrowserDownloadURL,
"apiUrl": a.APIURL,
"id": a.ID,
"name": a.Name,
"label": a.Label,
"size": a.Size,
"state": a.State,
"createdAt": a.CreatedAt,
"updatedAt": a.UpdatedAt,
"downloadCount": a.DownloadCount,
"contentType": a.ContentType,
})
}
data[f] = assets
default:
sf := fieldByName(v, f)
data[f] = sf.Interface()
}
}
return &data
}
// FetchRelease finds a repository release by its tagName.
func FetchRelease(httpClient *http.Client, baseRepo ghrepo.Interface, tagName string) (*Release, error) {
path := fmt.Sprintf("repos/%s/%s/releases/tags/%s", baseRepo.RepoOwner(), baseRepo.RepoName(), tagName)
url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 404 {
return FindDraftRelease(httpClient, baseRepo, tagName)
}
if resp.StatusCode > 299 {
return nil, api.HandleHTTPError(resp)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var release Release
err = json.Unmarshal(b, &release)
if err != nil {
return nil, err
}
return &release, nil
}
// FetchLatestRelease finds the latest published release for a repository.
func FetchLatestRelease(httpClient *http.Client, baseRepo ghrepo.Interface) (*Release, error) {
path := fmt.Sprintf("repos/%s/%s/releases/latest", baseRepo.RepoOwner(), baseRepo.RepoName())
url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode > 299 {
return nil, api.HandleHTTPError(resp)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var release Release
err = json.Unmarshal(b, &release)
if err != nil {
return nil, err
}
return &release, nil
}
// FindDraftRelease returns the latest draft release that matches tagName.
func FindDraftRelease(httpClient *http.Client, baseRepo ghrepo.Interface, tagName string) (*Release, error) {
path := fmt.Sprintf("repos/%s/%s/releases", baseRepo.RepoOwner(), baseRepo.RepoName())
url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
perPage := 100
page := 1
for {
req, err := http.NewRequest("GET", fmt.Sprintf("%s?per_page=%d&page=%d", url, perPage, page), nil)
if err != nil {
return nil, err
}
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode > 299 {
return nil, api.HandleHTTPError(resp)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var releases []Release
err = json.Unmarshal(b, &releases)
if err != nil {
return nil, err
}
for _, r := range releases {
if r.IsDraft && r.TagName == tagName {
return &r, nil
}
}
//nolint:staticcheck
break
}
return nil, errors.New("release not found")
}