github.com/mithrandie/csvq@v1.18.1/lib/action/update.go (about)

     1  package action
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"runtime"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/mithrandie/csvq/lib/query"
    15  )
    16  
    17  const githubApiLatestReleaseURL = "https://api.github.com/repos/mithrandie/csvq/releases/latest"
    18  const githubApiLatestPreReleaseURL = "https://api.github.com/repos/mithrandie/csvq/releases?per_page=1"
    19  const preReleaseIdentifier = "pr"
    20  
    21  type GithubRelease struct {
    22  	HTMLURL     string               `json:"html_url"`
    23  	TagName     string               `json:"tag_name"`
    24  	PublishedAt string               `json:"published_at"`
    25  	Assets      []GithubReleaseAsset `json:"assets"`
    26  }
    27  
    28  type GithubReleaseAsset struct {
    29  	Name               string `json:"name"`
    30  	BrowserDownloadURL string `json:"browser_download_url"`
    31  }
    32  
    33  var CurrentVersion = &Version{}
    34  
    35  type Version struct {
    36  	Major      int
    37  	Minor      int
    38  	Patch      int
    39  	PreRelease int
    40  }
    41  
    42  func (v *Version) IsEmpty() bool {
    43  	return v.Major == 0 && v.Minor == 0 && v.Patch == 0 && v.PreRelease == 0
    44  }
    45  
    46  func (v *Version) IsLaterThan(v2 *Version) bool {
    47  	if v == nil || v2 == nil {
    48  		return false
    49  	}
    50  
    51  	if v.Major != v2.Major {
    52  		return v.Major > v2.Major
    53  	}
    54  	if v.Minor != v2.Minor {
    55  		return v.Minor > v2.Minor
    56  	}
    57  	if v.Patch != v2.Patch {
    58  		return v.Patch > v2.Patch
    59  	}
    60  	if v.PreRelease != v2.PreRelease {
    61  		if v.PreRelease == 0 {
    62  			return true
    63  		}
    64  		if v2.PreRelease == 0 {
    65  			return false
    66  		}
    67  		return v.PreRelease > v2.PreRelease
    68  	}
    69  	return false
    70  }
    71  
    72  func (v *Version) String() string {
    73  	if v.PreRelease == 0 {
    74  		return strings.Join([]string{strconv.Itoa(v.Major), strconv.Itoa(v.Minor), strconv.Itoa(v.Patch)}, ".")
    75  	}
    76  	return strings.Join([]string{strconv.Itoa(v.Major), strconv.Itoa(v.Minor), strconv.Itoa(v.Patch)}, ".") +
    77  		"-" +
    78  		strings.Join([]string{preReleaseIdentifier, strconv.Itoa(v.PreRelease)}, ".")
    79  
    80  }
    81  
    82  func ParseVersion(s string) (*Version, error) {
    83  	v := &Version{}
    84  
    85  	s = PickVersionNumber(s)
    86  	words := strings.Split(s, "-")
    87  	rVer := strings.Split(words[0], ".")
    88  
    89  	if len(rVer) != 3 {
    90  		return v, errors.New("cannot parse to version")
    91  	}
    92  
    93  	major, err := strconv.Atoi(rVer[0])
    94  	if err != nil {
    95  		return v, errors.New("cannot parse to version")
    96  	}
    97  
    98  	minor, err := strconv.Atoi(rVer[1])
    99  	if err != nil {
   100  		return v, errors.New("cannot parse to version")
   101  	}
   102  
   103  	patch, err := strconv.Atoi(rVer[2])
   104  	if err != nil {
   105  		return v, errors.New("cannot parse to version")
   106  	}
   107  
   108  	preRelease := 0
   109  	if 1 < len(words) {
   110  		prVer := strings.Split(words[1], ".")
   111  		if len(prVer) != 2 {
   112  			return v, errors.New("cannot parse to version")
   113  		}
   114  		if prVer[0] != preReleaseIdentifier {
   115  			return v, errors.New("cannot parse to version")
   116  		}
   117  		preRelease, err = strconv.Atoi(prVer[1])
   118  		if err != nil {
   119  			return v, errors.New("cannot parse to version")
   120  		}
   121  	}
   122  
   123  	v.Major = major
   124  	v.Minor = minor
   125  	v.Patch = patch
   126  	v.PreRelease = preRelease
   127  	return v, nil
   128  }
   129  
   130  type GithubClient interface {
   131  	GetLatestRelease() (*GithubRelease, error)
   132  	GetLatestReleaseIncludingPreRelease() (*GithubRelease, error)
   133  }
   134  
   135  type Client struct{}
   136  
   137  func NewClient() GithubClient {
   138  	return &Client{}
   139  }
   140  
   141  func (c Client) GetLatestRelease() (*GithubRelease, error) {
   142  	res, err := http.Get(githubApiLatestReleaseURL)
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  
   147  	release := &GithubRelease{}
   148  	body, _ := io.ReadAll(res.Body)
   149  	err = json.Unmarshal(body, &release)
   150  	return release, err
   151  }
   152  
   153  func (c Client) GetLatestReleaseIncludingPreRelease() (*GithubRelease, error) {
   154  	res, err := http.Get(githubApiLatestPreReleaseURL)
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	release := []*GithubRelease{{}}
   160  	body, _ := io.ReadAll(res.Body)
   161  	err = json.Unmarshal(body, &release)
   162  	return release[0], err
   163  }
   164  
   165  func PickVersionNumber(s string) string {
   166  	if 0 < len(s) && s[0] == 'v' {
   167  		s = s[1:]
   168  	}
   169  	return s
   170  }
   171  
   172  func CheckUpdate(includePreRelaese bool) error {
   173  	msg, err := CheckForUpdates(includePreRelaese, NewClient(), runtime.GOOS, runtime.GOARCH)
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	return query.NewSession().WriteToStdoutWithLineBreak(msg)
   179  }
   180  
   181  func CheckForUpdates(includePreRelease bool, client GithubClient, goos string, goarch string) (string, error) {
   182  	var rel *GithubRelease
   183  	var err error
   184  
   185  	if includePreRelease {
   186  		rel, err = client.GetLatestReleaseIncludingPreRelease()
   187  	} else {
   188  		rel, err = client.GetLatestRelease()
   189  	}
   190  	if err != nil {
   191  		return "", err
   192  	}
   193  
   194  	latestVersion, _ := ParseVersion(rel.TagName)
   195  	if latestVersion.IsEmpty() {
   196  		return "", errors.New(fmt.Sprintf("Invalid release number: %s", rel.TagName))
   197  	}
   198  
   199  	publishedAt := ""
   200  	if publishedTime, err := time.Parse(time.RFC3339, rel.PublishedAt); err == nil {
   201  		publishedAt = publishedTime.Format("Jan 02, 2006")
   202  	}
   203  
   204  	if CurrentVersion.IsEmpty() {
   205  		return fmt.Sprintf("The current version is an invalid number.\nThe latest version is %s, released on %s.\n  Release URL: %s", latestVersion.String(), publishedAt, rel.HTMLURL), nil
   206  	}
   207  
   208  	if !latestVersion.IsLaterThan(CurrentVersion) {
   209  		return fmt.Sprintf("The current version %s is up to date.", CurrentVersion.String()), nil
   210  	}
   211  
   212  	msg := fmt.Sprintf("Version %s is now available.\n  Release Date: %s\n  Release URL:  %s", latestVersion.String(), publishedAt, rel.HTMLURL)
   213  
   214  	archiveName := fmt.Sprintf("csvq-v%s-%s-%s.tar.gz", latestVersion.String(), goos, goarch)
   215  	for _, assets := range rel.Assets {
   216  		if archiveName == assets.Name {
   217  			msg = msg + fmt.Sprintf("\n  Download URL: %s", assets.BrowserDownloadURL)
   218  		}
   219  	}
   220  
   221  	return msg, nil
   222  }