From c2a84645125196e458f570bed414c4cfff1d29a6 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Thu, 28 Mar 2024 10:36:39 +0100 Subject: [PATCH] Fix cli version comparison and improve setup command (#3518) --- cli/common/hooks.go | 4 +- cli/setup/setup.go | 9 ++++- cli/setup/token_fetcher.go | 15 ++++++++ cli/setup/ui/ask.go | 79 +++++++------------------------------- cli/setup/ui/confirm.go | 68 ++++---------------------------- cli/update/types.go | 15 ++++---- cli/update/updater.go | 47 ++++++++++++----------- cli/update/updater_test.go | 61 +++++++++++++++++++++++++++++ go.mod | 9 +++-- go.sum | 10 ++++- web/src/views/cli/Auth.vue | 3 +- 11 files changed, 155 insertions(+), 165 deletions(-) create mode 100644 cli/update/updater_test.go diff --git a/cli/common/hooks.go b/cli/common/hooks.go index 5ff2a1242..5febcfa13 100644 --- a/cli/common/hooks.go +++ b/cli/common/hooks.go @@ -37,7 +37,7 @@ func Before(c *cli.Context) error { log.Debug().Msg("Checking for updates ...") - newVersion, err := update.CheckForUpdate(waitForUpdateCheck, true) + newVersion, err := update.CheckForUpdate(waitForUpdateCheck, false) if err != nil { log.Error().Err(err).Msgf("Failed to check for updates") return @@ -58,7 +58,7 @@ func After(_ *cli.Context) error { select { case <-waitForUpdateCheck.Done(): // When the actual command already finished, we still wait 250ms for the update check to finish - case <-time.After(time.Millisecond * 250): + case <-time.After(time.Millisecond * 500): log.Debug().Msg("Update check stopped due to timeout") cancelWaitForUpdate(errors.New("update check timeout")) } diff --git a/cli/setup/setup.go b/cli/setup/setup.go index b114cf750..bdacd0127 100644 --- a/cli/setup/setup.go +++ b/cli/setup/setup.go @@ -13,8 +13,10 @@ import ( // Command exports the setup command. var Command = &cli.Command{ - Name: "setup", - Usage: "setup the woodpecker-cli for the first time", + Name: "setup", + Usage: "setup the woodpecker-cli for the first time", + Args: true, + ArgsUsage: "[server-url]", Flags: []cli.Flag{ &cli.StringFlag{ Name: "server-url", @@ -45,6 +47,9 @@ func setup(c *cli.Context) error { } serverURL := c.String("server-url") + if serverURL == "" { + serverURL = c.Args().First() + } if serverURL == "" { serverURL, err = ui.Ask("Enter the URL of the woodpecker server", "https://ci.woodpecker-ci.org", true) diff --git a/cli/setup/token_fetcher.go b/cli/setup/token_fetcher.go index e96772cac..59be6b08d 100644 --- a/cli/setup/token_fetcher.go +++ b/cli/setup/token_fetcher.go @@ -10,6 +10,7 @@ import ( "runtime" "time" + "github.com/charmbracelet/huh/spinner" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" ) @@ -37,13 +38,27 @@ func receiveTokenFromUI(c context.Context, serverURL string) (string, error) { return "", err } + spinnerCtx, spinnerDone := context.WithCancelCause(c) + go func() { + err = spinner.New(). + Title("Waiting for token ..."). + Context(spinnerCtx). + Run() + if err != nil { + return + } + }() + // wait for token to be received or timeout select { case token := <-tokenReceived: + spinnerDone(nil) return token, nil case <-c.Done(): + spinnerDone(nil) return "", c.Err() case <-time.After(5 * time.Minute): + spinnerDone(nil) return "", errors.New("timed out waiting for token") } } diff --git a/cli/setup/ui/ask.go b/cli/setup/ui/ask.go index 2fa8c539e..eea4b2b00 100644 --- a/cli/setup/ui/ask.go +++ b/cli/setup/ui/ask.go @@ -1,79 +1,26 @@ package ui import ( - "fmt" + "errors" "strings" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" ) -type askModel struct { - prompt string - required bool - textInput textinput.Model - err error -} - -func (m askModel) Init() tea.Cmd { - return textinput.Blink -} - -func (m askModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.Type { - case tea.KeyEnter: - if !m.required || (m.required && strings.TrimSpace(m.textInput.Value()) != "") { - return m, tea.Quit - } - case tea.KeyCtrlC, tea.KeyEsc: - return m, tea.Quit - } - default: - return m, cmd - } - - m.textInput, cmd = m.textInput.Update(msg) - return m, cmd -} - -func (m askModel) View() string { - return fmt.Sprintf( - "%s\n\n%s\n\n%s", - m.prompt, - m.textInput.View(), - "(esc to quit)", - ) + "\n" -} - func Ask(prompt, placeholder string, required bool) (string, error) { - ti := textinput.New() - ti.Placeholder = placeholder - ti.Focus() - ti.CharLimit = 156 - ti.Width = 40 - - p := tea.NewProgram(askModel{ - prompt: prompt, - textInput: ti, - required: required, - err: nil, - }) - - _m, err := p.Run() + var input string + err := huh.NewInput(). + Title(prompt). + Value(&input). + Placeholder(placeholder).Validate(func(s string) error { + if required && strings.TrimSpace(s) == "" { + return errors.New("required") + } + return nil + }).Run() if err != nil { return "", err } - m, ok := _m.(askModel) - if !ok { - return "", fmt.Errorf("unexpected model: %T", _m) - } - - text := strings.TrimSpace(m.textInput.Value()) - - return text, nil + return strings.TrimSpace(input), nil } diff --git a/cli/setup/ui/confirm.go b/cli/setup/ui/confirm.go index 350fdb2e4..1a7dfa494 100644 --- a/cli/setup/ui/confirm.go +++ b/cli/setup/ui/confirm.go @@ -1,71 +1,19 @@ package ui import ( - "fmt" - - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" ) -type confirmModel struct { - confirmed bool - prompt string - err error -} - -func (m confirmModel) Init() tea.Cmd { - return textinput.Blink -} - -func (m confirmModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - - switch msg := msg.(type) { - case tea.KeyMsg: - if msg.Runes != nil { - switch msg.Runes[0] { - case 'y': - m.confirmed = true - return m, tea.Quit - case 'n': - m.confirmed = false - return m, tea.Quit - } - } - - switch msg.Type { - case tea.KeyCtrlC, tea.KeyEsc: - return m, tea.Quit - } - default: - return m, nil - } - - return m, cmd -} - -func (m confirmModel) View() string { - return fmt.Sprintf( - "%s y / n (esc to quit)", - m.prompt, - ) + "\n" -} - func Confirm(prompt string) (bool, error) { - p := tea.NewProgram(confirmModel{ - prompt: prompt, - err: nil, - }) - - _m, err := p.Run() + var confirm bool + err := huh.NewConfirm(). + Title(prompt). + Affirmative("Yes!"). + Negative("No."). + Value(&confirm).Run() if err != nil { return false, err } - m, ok := _m.(confirmModel) - if !ok { - return false, fmt.Errorf("unexpected model: %T", _m) - } - - return m.confirmed, nil + return confirm, err } diff --git a/cli/update/types.go b/cli/update/types.go index 35ae99181..7f2bf6b77 100644 --- a/cli/update/types.go +++ b/cli/update/types.go @@ -1,11 +1,9 @@ package update -type GithubRelease struct { - TagName string `json:"tag_name"` - Assets []struct { - Name string `json:"name"` - BrowserDownloadURL string `json:"browser_download_url"` - } `json:"assets"` +type VersionData struct { + Latest string `json:"latest"` + Next string `json:"next"` + RC string `json:"rc"` } type NewVersion struct { @@ -13,4 +11,7 @@ type NewVersion struct { AssetURL string } -const githubReleaseURL = "https://api.github.com/repos/woodpecker-ci/woodpecker/releases/latest" +const ( + woodpeckerVersionURL = "https://woodpecker-ci.org/version.json" + githubBinaryURL = "https://github.com/woodpecker-ci/woodpecker/releases/download/v%s/woodpecker-cli_%s_%s.tar.gz" +) diff --git a/cli/update/updater.go b/cli/update/updater.go index dce5b5d1c..41d6eb3b8 100644 --- a/cli/update/updater.go +++ b/cli/update/updater.go @@ -10,6 +10,7 @@ import ( "os" "path" "runtime" + "strings" "github.com/rs/zerolog/log" @@ -17,14 +18,18 @@ import ( ) func CheckForUpdate(ctx context.Context, force bool) (*NewVersion, error) { + return checkForUpdate(ctx, woodpeckerVersionURL, force) +} + +func checkForUpdate(ctx context.Context, versionURL string, force bool) (*NewVersion, error) { log.Debug().Msgf("Current version: %s", version.String()) - if version.String() == "dev" && !force { - log.Debug().Msgf("Skipping update check for development version") + if (version.String() == "dev" || strings.HasPrefix(version.String(), "next-")) && !force { + log.Debug().Msgf("Skipping update check for development & next versions") return nil, nil } - req, err := http.NewRequestWithContext(ctx, "GET", githubReleaseURL, nil) + req, err := http.NewRequestWithContext(ctx, "GET", versionURL, nil) if err != nil { return nil, err } @@ -39,34 +44,32 @@ func CheckForUpdate(ctx context.Context, force bool) (*NewVersion, error) { return nil, errors.New("failed to fetch the latest release") } - var release GithubRelease - if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + var versionData VersionData + if err := json.NewDecoder(resp.Body).Decode(&versionData); err != nil { return nil, err } + upstreamVersion := versionData.Latest + if strings.HasPrefix(version.String(), "next-") { + upstreamVersion = versionData.Next + } else if strings.HasSuffix(version.String(), "rc-") { + upstreamVersion = versionData.RC + } + + installedVersion := strings.TrimPrefix(version.Version, "v") + upstreamVersion = strings.TrimPrefix(upstreamVersion, "v") + // using the latest release - if release.TagName == version.String() && !force { + if installedVersion == upstreamVersion && !force { + log.Debug().Msgf("No new version available") return nil, nil } - log.Debug().Msgf("Latest version: %s", release.TagName) - - assetURL := "" - fileName := fmt.Sprintf("woodpecker-cli_%s_%s.tar.gz", runtime.GOOS, runtime.GOARCH) - for _, asset := range release.Assets { - if fileName == asset.Name { - assetURL = asset.BrowserDownloadURL - log.Debug().Msgf("Found asset for the current OS and arch: %s", assetURL) - break - } - } - - if assetURL == "" { - return nil, errors.New("no asset found for the current OS") - } + log.Debug().Msgf("New version available: %s", upstreamVersion) + assetURL := fmt.Sprintf(githubBinaryURL, upstreamVersion, runtime.GOOS, runtime.GOARCH) return &NewVersion{ - Version: release.TagName, + Version: upstreamVersion, AssetURL: assetURL, }, nil } diff --git a/cli/update/updater_test.go b/cli/update/updater_test.go new file mode 100644 index 000000000..a2855ac8e --- /dev/null +++ b/cli/update/updater_test.go @@ -0,0 +1,61 @@ +package update + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "go.woodpecker-ci.org/woodpecker/v2/version" +) + +func TestCheckForUpdate(t *testing.T) { + version.Version = "1.0.0" + fixtureHandler := func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/version.json" { + http.NotFound(w, r) + return + } + + _, _ = io.WriteString(w, `{"latest": "1.0.1", "next": "1.0.2", "rc": "1.0.3"}`) + } + ts := httptest.NewServer(http.HandlerFunc(fixtureHandler)) + defer ts.Close() + + newVersion, err := checkForUpdate(context.Background(), ts.URL+"/version.json", false) + if err != nil { + t.Fatalf("Failed to check for updates: %v", err) + } + + if newVersion == nil || newVersion.Version != "1.0.1" { + t.Fatalf("Expected a new version 1.0.1, got: %s", newVersion) + } +} + +func TestDownloadNewVersion(t *testing.T) { + downloadFilePath := "/woodpecker-cli_linux_amd64.tar.gz" + + fixtureHandler := func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != downloadFilePath { + http.NotFound(w, r) + return + } + + _, _ = io.WriteString(w, `blob`) + } + ts := httptest.NewServer(http.HandlerFunc(fixtureHandler)) + defer ts.Close() + + file, err := downloadNewVersion(context.Background(), ts.URL+downloadFilePath) + if err != nil { + t.Fatalf("Failed to download new version: %v", err) + } + + if file == "" { + t.Fatalf("Expected a file path, got: %s", file) + } + + _ = os.Remove(file) +} diff --git a/go.mod b/go.mod index cb7e010d0..2d7056dd1 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,8 @@ require ( github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/caddyserver/certmagic v0.20.0 github.com/cenkalti/backoff/v4 v4.2.1 - github.com/charmbracelet/bubbles v0.18.0 - github.com/charmbracelet/bubbletea v0.25.0 + github.com/charmbracelet/huh v0.3.0 + github.com/charmbracelet/huh/spinner v0.0.0-20240306161957-71f31c155b08 github.com/distribution/reference v0.5.0 github.com/docker/cli v24.0.9+incompatible github.com/docker/docker v24.0.9+incompatible @@ -81,7 +81,10 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.10.2 // indirect + github.com/catppuccin/go v0.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/charmbracelet/bubbles v0.18.0 // indirect + github.com/charmbracelet/bubbletea v0.25.0 // indirect github.com/charmbracelet/lipgloss v0.9.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect @@ -137,7 +140,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/morikuni/aec v1.0.0 // indirect - github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect diff --git a/go.sum b/go.sum index 6edbfdcb3..27e1b5e43 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZF github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc= github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -48,6 +50,10 @@ github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/ github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/huh v0.3.0 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE= +github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA= +github.com/charmbracelet/huh/spinner v0.0.0-20240306161957-71f31c155b08 h1:kO5eMMxyCJ6m7gdpGQ7OomrMdfsKVPgC4aB/focl/HE= +github.com/charmbracelet/huh/spinner v0.0.0-20240306161957-71f31c155b08/go.mod h1:nrBG0YEHaxdbqHXW1xvG1hPqkuac9Eg7RTMvogiXuz0= github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= @@ -345,8 +351,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= diff --git a/web/src/views/cli/Auth.vue b/web/src/views/cli/Auth.vue index fd5b1c793..bc4da4648 100644 --- a/web/src/views/cli/Auth.vue +++ b/web/src/views/cli/Auth.vue @@ -1,7 +1,7 @@