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  }