@@ -17,7 +17,9 @@ import (
1717 "os/exec"
1818 "path"
1919 "path/filepath"
20+ "regexp"
2021 "runtime"
22+ "strconv"
2123 "strings"
2224 "time"
2325
@@ -139,16 +141,26 @@ func (u *updater) Run(ctx context.Context, force bool, coderURLArg string, versi
139141 currentVersion , err := semver .NewVersion (u .versionF ())
140142 if err != nil {
141143 clog .LogWarn ("failed to determine current version of coder-cli" , clog .Causef (err .Error ()))
142- } else if currentVersion . Compare ( desiredVersion ) == 0 {
144+ } else if compareVersions ( currentVersion , desiredVersion ) == 0 {
143145 clog .LogInfo ("Up to date!" )
144146 return nil
145147 }
146148
147149 if ! force {
148- label := fmt .Sprintf ("Do you want to download version %d.%d.%d instead" ,
150+ prerelease := ""
151+ if desiredVersion .Prerelease () != "" {
152+ prerelease = "-" + desiredVersion .Prerelease ()
153+ }
154+ hotfix := ""
155+ if hotfixVersion (desiredVersion ) != "" {
156+ hotfix = hotfixVersion (desiredVersion )
157+ }
158+ label := fmt .Sprintf ("Do you want to download version %d.%d.%d%s%s instead" ,
149159 desiredVersion .Major (),
150160 desiredVersion .Minor (),
151161 desiredVersion .Patch (),
162+ prerelease ,
163+ hotfix ,
152164 )
153165 if _ , err := u .confirmF (label ); err != nil {
154166 return clog .Fatal ("user cancelled operation" , clog .Tipf (`use "--force" to update without confirmation` ))
@@ -218,7 +230,7 @@ func (u *updater) Run(ctx context.Context, force bool, coderURLArg string, versi
218230 return clog .Fatal ("failed to update coder binary" , clog .Causef (err .Error ()))
219231 }
220232
221- clog .LogSuccess ("Updated coder CLI to version " + desiredVersion . String () )
233+ clog .LogSuccess ("Updated coder CLI" )
222234 return nil
223235}
224236
@@ -308,6 +320,7 @@ func queryGithubAssetURL(httpClient getter, version *semver.Version, ostype stri
308320 fmt .Fprint (& b , "-" )
309321 fmt .Fprint (& b , version .Prerelease ())
310322 }
323+ fmt .Fprintf (& b , "%s" , hotfixVersion (version )) // this will be empty if no hotfix
311324
312325 urlString := fmt .Sprintf ("https://api.github.com/repos/cdr/coder-cli/releases/tags/v%s" , b .String ())
313326 clog .LogInfo ("query github releases" , fmt .Sprintf ("url: %q" , urlString ))
@@ -493,3 +506,76 @@ func HasFilePathPrefix(s, prefix string) bool {
493506func defaultExec (ctx context.Context , cmd string , args ... string ) ([]byte , error ) {
494507 return exec .CommandContext (ctx , cmd , args ... ).CombinedOutput ()
495508}
509+
510+ // hotfixExpr matches the build metadata used for identifying CLI hotfixes.
511+ var hotfixExpr = regexp .MustCompile (`(?i)^.*?cli\.(\d+).*?$` )
512+
513+ // hotfixVersion returns the hotfix build metadata tag if it is present in v
514+ // and an empty string otherwise.
515+ func hotfixVersion (v * semver.Version ) string {
516+ match := hotfixExpr .FindStringSubmatch (v .Metadata ())
517+ if len (match ) < 2 {
518+ return ""
519+ }
520+
521+ return fmt .Sprintf ("+cli.%s" , match [1 ])
522+ }
523+
524+ // compareVersions performs a NON-SEMVER-COMPLIANT comparison of two versions.
525+ // If the two versions differ as per SemVer, then that result is returned.
526+ // Otherwise, the build metadata of the two versions are compared based on
527+ // the `cli.N` hotfix metadata.
528+ //
529+ // Examples:
530+ // compareVersions(semver.MustParse("v1.0.0"), semver.MustParse("v1.0.0"))
531+ // 0
532+ // compareVersions(semver.MustParse("v1.0.0"), semver.MustParse("v1.0.1"))
533+ // 1
534+ // compareVersions(semver.MustParse("v1.0.1"), semver.MustParse("v1.0.0"))
535+ // -1
536+ // compareVersions(semver.MustParse("v1.0.0+cli.0"), semver.MustParse("v1.0.0"))
537+ // 1
538+ // compareVersions(semver.MustParse("v1.0.0+cli.0"), semver.MustParse("v1.0.0+cli.0"))
539+ // 0
540+ // compareVersions(semver.MustParse("v1.0.0"), semver.MustParse("v1.0.0+cli.0"))
541+ // -1
542+ // compareVersions(semver.MustParse("v1.0.0+cli.1"), semver.MustParse("v1.0.0+cli.0"))
543+ // 1
544+ // compareVersions(semver.MustParse("v1.0.0+cli.0"), semver.MustParse("v1.0.0+cli.1"))
545+ // -1
546+ //
547+ func compareVersions (a , b * semver.Version ) int {
548+ semverComparison := a .Compare (b )
549+ if semverComparison != 0 {
550+ return semverComparison
551+ }
552+
553+ matchA := hotfixExpr .FindStringSubmatch (a .Metadata ())
554+ matchB := hotfixExpr .FindStringSubmatch (b .Metadata ())
555+
556+ hotfixA := - 1
557+ hotfixB := - 1
558+
559+ // extract hotfix versions from the metadata of a and b
560+ if len (matchA ) > 1 {
561+ if n , err := strconv .Atoi (matchA [1 ]); err == nil {
562+ hotfixA = n
563+ }
564+ }
565+ if len (matchB ) > 1 {
566+ if n , err := strconv .Atoi (matchB [1 ]); err == nil {
567+ hotfixB = n
568+ }
569+ }
570+
571+ // compare hotfix versions
572+ if hotfixA < hotfixB {
573+ return - 1
574+ }
575+ if hotfixA > hotfixB {
576+ return 1
577+ }
578+ // both versions are the same if their semver and hotfix
579+ // metadata are the same.
580+ return 0
581+ }
0 commit comments