1package shared 2 3import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "io/ioutil" 8 "net/http" 9 "reflect" 10 "strings" 11 "time" 12 13 "github.com/cli/cli/v2/api" 14 "github.com/cli/cli/v2/internal/ghinstance" 15 "github.com/cli/cli/v2/internal/ghrepo" 16) 17 18var ReleaseFields = []string{ 19 "url", 20 "apiUrl", 21 "uploadUrl", 22 "tarballUrl", 23 "zipballUrl", 24 "id", 25 "tagName", 26 "name", 27 "body", 28 "isDraft", 29 "isPrerelease", 30 "createdAt", 31 "publishedAt", 32 "targetCommitish", 33 "author", 34 "assets", 35} 36 37type Release struct { 38 ID string `json:"node_id"` 39 TagName string `json:"tag_name"` 40 Name string `json:"name"` 41 Body string `json:"body"` 42 IsDraft bool `json:"draft"` 43 IsPrerelease bool `json:"prerelease"` 44 CreatedAt time.Time `json:"created_at"` 45 PublishedAt *time.Time `json:"published_at"` 46 47 TargetCommitish string `json:"target_commitish"` 48 49 APIURL string `json:"url"` 50 UploadURL string `json:"upload_url"` 51 TarballURL string `json:"tarball_url"` 52 ZipballURL string `json:"zipball_url"` 53 URL string `json:"html_url"` 54 Assets []ReleaseAsset 55 56 Author struct { 57 ID string `json:"node_id"` 58 Login string `json:"login"` 59 } 60} 61 62type ReleaseAsset struct { 63 ID string `json:"node_id"` 64 Name string 65 Label string 66 Size int64 67 State string 68 APIURL string `json:"url"` 69 70 CreatedAt time.Time `json:"created_at"` 71 UpdatedAt time.Time `json:"updated_at"` 72 DownloadCount int `json:"download_count"` 73 ContentType string `json:"content_type"` 74 BrowserDownloadURL string `json:"browser_download_url"` 75} 76 77func (rel *Release) ExportData(fields []string) map[string]interface{} { 78 v := reflect.ValueOf(rel).Elem() 79 fieldByName := func(v reflect.Value, field string) reflect.Value { 80 return v.FieldByNameFunc(func(s string) bool { 81 return strings.EqualFold(field, s) 82 }) 83 } 84 data := map[string]interface{}{} 85 86 for _, f := range fields { 87 switch f { 88 case "author": 89 data[f] = map[string]interface{}{ 90 "id": rel.Author.ID, 91 "login": rel.Author.Login, 92 } 93 case "assets": 94 assets := make([]interface{}, 0, len(rel.Assets)) 95 for _, a := range rel.Assets { 96 assets = append(assets, map[string]interface{}{ 97 "url": a.BrowserDownloadURL, 98 "apiUrl": a.APIURL, 99 "id": a.ID, 100 "name": a.Name, 101 "label": a.Label, 102 "size": a.Size, 103 "state": a.State, 104 "createdAt": a.CreatedAt, 105 "updatedAt": a.UpdatedAt, 106 "downloadCount": a.DownloadCount, 107 "contentType": a.ContentType, 108 }) 109 } 110 data[f] = assets 111 default: 112 sf := fieldByName(v, f) 113 data[f] = sf.Interface() 114 } 115 } 116 117 return data 118} 119 120// FetchRelease finds a repository release by its tagName. 121func FetchRelease(httpClient *http.Client, baseRepo ghrepo.Interface, tagName string) (*Release, error) { 122 path := fmt.Sprintf("repos/%s/%s/releases/tags/%s", baseRepo.RepoOwner(), baseRepo.RepoName(), tagName) 123 url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path 124 req, err := http.NewRequest("GET", url, nil) 125 if err != nil { 126 return nil, err 127 } 128 129 resp, err := httpClient.Do(req) 130 if err != nil { 131 return nil, err 132 } 133 defer resp.Body.Close() 134 135 if resp.StatusCode == 404 { 136 return FindDraftRelease(httpClient, baseRepo, tagName) 137 } 138 139 if resp.StatusCode > 299 { 140 return nil, api.HandleHTTPError(resp) 141 } 142 143 b, err := ioutil.ReadAll(resp.Body) 144 if err != nil { 145 return nil, err 146 } 147 148 var release Release 149 err = json.Unmarshal(b, &release) 150 if err != nil { 151 return nil, err 152 } 153 154 return &release, nil 155} 156 157// FetchLatestRelease finds the latest published release for a repository. 158func FetchLatestRelease(httpClient *http.Client, baseRepo ghrepo.Interface) (*Release, error) { 159 path := fmt.Sprintf("repos/%s/%s/releases/latest", baseRepo.RepoOwner(), baseRepo.RepoName()) 160 url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path 161 req, err := http.NewRequest("GET", url, nil) 162 if err != nil { 163 return nil, err 164 } 165 166 resp, err := httpClient.Do(req) 167 if err != nil { 168 return nil, err 169 } 170 defer resp.Body.Close() 171 172 if resp.StatusCode > 299 { 173 return nil, api.HandleHTTPError(resp) 174 } 175 176 b, err := ioutil.ReadAll(resp.Body) 177 if err != nil { 178 return nil, err 179 } 180 181 var release Release 182 err = json.Unmarshal(b, &release) 183 if err != nil { 184 return nil, err 185 } 186 187 return &release, nil 188} 189 190// FindDraftRelease returns the latest draft release that matches tagName. 191func FindDraftRelease(httpClient *http.Client, baseRepo ghrepo.Interface, tagName string) (*Release, error) { 192 path := fmt.Sprintf("repos/%s/%s/releases", baseRepo.RepoOwner(), baseRepo.RepoName()) 193 url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path 194 195 perPage := 100 196 page := 1 197 for { 198 req, err := http.NewRequest("GET", fmt.Sprintf("%s?per_page=%d&page=%d", url, perPage, page), nil) 199 if err != nil { 200 return nil, err 201 } 202 203 resp, err := httpClient.Do(req) 204 if err != nil { 205 return nil, err 206 } 207 defer resp.Body.Close() 208 209 if resp.StatusCode > 299 { 210 return nil, api.HandleHTTPError(resp) 211 } 212 213 b, err := ioutil.ReadAll(resp.Body) 214 if err != nil { 215 return nil, err 216 } 217 218 var releases []Release 219 err = json.Unmarshal(b, &releases) 220 if err != nil { 221 return nil, err 222 } 223 224 for _, r := range releases { 225 if r.IsDraft && r.TagName == tagName { 226 return &r, nil 227 } 228 } 229 //nolint:staticcheck 230 break 231 } 232 233 return nil, errors.New("release not found") 234} 235