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 }