github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/release/github/actions.go (about) 1 // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package github 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "io" 11 "log" 12 "net/http" 13 "net/url" 14 "os" 15 "regexp" 16 "strconv" 17 "time" 18 ) 19 20 // CreateRelease creates a release for a tag 21 func CreateRelease(token string, repo string, tag string, name string) error { 22 params := ReleaseCreate{ 23 TagName: tag, 24 Name: name, 25 } 26 27 payload, err := json.Marshal(params) 28 if err != nil { 29 return fmt.Errorf("can't encode release creation params, %v", err) 30 } 31 reader := bytes.NewReader(payload) 32 33 uri := fmt.Sprintf("/repos/keybase/%s/releases", repo) 34 resp, err := DoAuthRequest("POST", githubAPIURL+uri, "application/json", token, nil, reader) 35 if resp != nil { 36 defer func() { _ = resp.Body.Close() }() 37 } 38 if err != nil { 39 return fmt.Errorf("while submitting %v, %v", string(payload), err) 40 } 41 if resp.StatusCode != http.StatusCreated { 42 if resp.StatusCode == 422 { 43 return fmt.Errorf("github returned %v (this is probably because the release already exists)", 44 resp.Status) 45 } 46 return fmt.Errorf("github returned %v", resp.Status) 47 } 48 return nil 49 } 50 51 // Upload uploads a file to a tagged repo 52 func Upload(token string, repo string, tag string, name string, file string) error { 53 release, err := ReleaseOfTag("keybase", repo, tag, token) 54 if err != nil { 55 return err 56 } 57 v := url.Values{} 58 v.Set("name", name) 59 url := release.CleanUploadURL() + "?" + v.Encode() 60 osfile, err := os.Open(file) 61 if err != nil { 62 return err 63 } 64 resp, err := DoAuthRequest("POST", url, "application/octet-stream", token, nil, osfile) 65 if resp != nil { 66 defer func() { _ = resp.Body.Close() }() 67 } 68 if err != nil { 69 return err 70 } 71 if resp.StatusCode != http.StatusCreated { 72 if resp.StatusCode == 422 { 73 return fmt.Errorf("github returned %v (this is probably because the release already exists)", 74 resp.Status) 75 } 76 return fmt.Errorf("github returned %v", resp.Status) 77 } 78 return nil 79 } 80 81 // DownloadSource dowloads source from repo tag 82 func DownloadSource(token string, repo string, tag string) error { 83 url := githubAPIURL + fmt.Sprintf("/repos/keybase/%s/tarball/%s", repo, tag) 84 name := fmt.Sprintf("%s-%s.tar.gz", repo, tag) 85 log.Printf("Url: %s", url) 86 return Download(token, url, name) 87 } 88 89 // DownloadAsset downloads an asset from Github that matches name 90 func DownloadAsset(token string, repo string, tag string, name string) error { 91 release, err := ReleaseOfTag("keybase", repo, tag, token) 92 if err != nil { 93 return err 94 } 95 96 assetID := 0 97 for _, asset := range release.Assets { 98 if asset.Name == name { 99 assetID = asset.ID 100 } 101 } 102 103 if assetID == 0 { 104 return fmt.Errorf("could not find asset named %s", name) 105 } 106 107 url := githubAPIURL + fmt.Sprintf(assetDownloadURI, "keybase", repo, assetID) 108 return Download(token, url, name) 109 } 110 111 // Download from Github 112 func Download(token string, url string, name string) error { 113 resp, err := DoAuthRequest("GET", url, "", token, map[string]string{ 114 "Accept": "application/octet-stream", 115 }, nil) 116 if resp != nil { 117 defer func() { _ = resp.Body.Close() }() 118 } 119 if err != nil { 120 return fmt.Errorf("could not fetch releases, %v", err) 121 } 122 123 contentLength, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64) 124 if err != nil { 125 return err 126 } 127 128 if resp.StatusCode != http.StatusOK { 129 return fmt.Errorf("github did not respond with 200 OK but with %v", resp.Status) 130 } 131 132 out, err := os.Create(name) 133 if err != nil { 134 return fmt.Errorf("could not create file %s", name) 135 } 136 defer func() { _ = out.Close() }() 137 138 n, err := io.Copy(out, resp.Body) 139 if n != contentLength { 140 return fmt.Errorf("downloaded data did not match content length %d != %d", contentLength, n) 141 } 142 return err 143 } 144 145 // LatestCommit returns a latest commit for all statuses matching state and contexts 146 func LatestCommit(token string, repo string, contexts []string) (*Commit, error) { 147 commits, err := Commits("keybase", repo, token) 148 if err != nil { 149 return nil, err 150 } 151 152 for _, commit := range commits { 153 log.Printf("Checking %s", commit.SHA) 154 statuses, err := getStatuses(token, "keybase", repo, commit.SHA) 155 if err != nil { 156 return nil, err 157 } 158 matching := map[string]Status{} 159 for _, status := range statuses { 160 if stringInSlice(status.Context, contexts) { 161 switch status.State { 162 case "failure": 163 log.Printf("%s (failure)", status.Context) 164 case "success": 165 log.Printf("%s (success)", status.Context) 166 matching[status.Context] = status 167 } 168 } 169 } 170 // If we match all contexts then we've found the commit 171 if len(contexts) == len(matching) { 172 return &commit, nil 173 } 174 } 175 return nil, nil 176 } 177 178 func stringInSlice(str string, list []string) bool { 179 for _, s := range list { 180 if s == str { 181 return true 182 } 183 } 184 return false 185 } 186 187 // CIStatuses lists statuses for CI 188 func CIStatuses(token string, repo string, commit string) error { 189 log.Printf("Statuses for %s, %q\n", repo, commit) 190 statuses, err := getStatuses(token, "keybase", repo, commit) 191 if err != nil { 192 return err 193 } 194 log.Println("\tStatuses:") 195 for _, status := range statuses { 196 log.Printf("\t%s (%s)", status.Context, status.State) 197 } 198 return nil 199 } 200 201 // WaitForCI waits for commit in repo to pass CI contexts 202 func WaitForCI(token string, repo string, commit string, contexts []string, delay time.Duration, timeout time.Duration) error { 203 start := time.Now() 204 re := regexp.MustCompile("(.*)(/label=.*)") 205 for time.Since(start) < timeout { 206 log.Printf("Checking status for %s, %q (%s)", repo, contexts, commit) 207 statuses, err := overallStatus(token, "keybase", repo, commit) 208 if err != nil { 209 return err 210 } 211 const successStatus = "success" 212 const failureStatus = "failure" 213 const errorStatus = "error" 214 215 // See if the topmost, overall status has passed 216 log.Println("\tOverall:", statuses.State) 217 218 matching := map[string]Status{} 219 log.Println("\tStatuses:") 220 for _, status := range statuses.Statuses { 221 log.Printf("\t%s (%s)", status.Context, status.State) 222 } 223 log.Println("\t") 224 log.Println("\tMatch:") 225 226 // Fill in successes for all contexts first 227 for _, status := range statuses.Statuses { 228 context := re.ReplaceAllString(status.Context, "$1") 229 if stringInSlice(context, contexts) && status.State == successStatus { 230 log.Printf("\t%s (success)", context) 231 matching[context] = status 232 } 233 } 234 235 // Check failures and errors. If we had a success for that context, 236 // we can ignore them. Otherwise we'll fail right away. 237 for _, status := range statuses.Statuses { 238 context := re.ReplaceAllString(status.Context, "$1") 239 if stringInSlice(context, contexts) { 240 switch status.State { 241 case failureStatus, errorStatus: 242 if matching[context].State != successStatus { 243 log.Printf("\t%s (%s)", context, status.State) 244 return fmt.Errorf("Failure in CI for %s", context) 245 } 246 log.Printf("\t%s (ignoring previous failure)", context) 247 } 248 } 249 } 250 log.Println("\t") 251 // If we match all contexts then we've passed 252 if len(contexts) == len(matching) { 253 return nil 254 } 255 256 log.Printf("Waiting %s", delay) 257 time.Sleep(delay) 258 } 259 return fmt.Errorf("Timed out") 260 }