Fix cli version comparison and improve setup command (#3518)

This commit is contained in:
Anbraten 2024-03-28 10:36:39 +01:00 committed by GitHub
parent 0b76e465c1
commit c2a8464512
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 155 additions and 165 deletions

View file

@ -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"))
}

View file

@ -15,6 +15,8 @@ import (
var Command = &cli.Command{
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)

View file

@ -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")
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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"
)

View file

@ -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
}

View file

@ -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)
}

9
go.mod
View file

@ -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

10
go.sum
View file

@ -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=

View file

@ -1,7 +1,7 @@
<template>
<div class="flex flex-col gap-4 m-auto">
<div class="text-center text-wp-text-100">
<img src="../../assets/logo.svg" alt="CLI" class="w-32 m-auto mb-8" />
<WoodpeckerLogo preserveAspectRatio="xMinYMin slice" class="w-32 m-auto mb-8" />
<template v-if="state === 'confirm'">
<h1 class="text-4xl font-bold">{{ $t('login_to_cli') }}</h1>
<p class="text-2xl">{{ $t('login_to_cli_description') }}</p>
@ -32,6 +32,7 @@ import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import WoodpeckerLogo from '~/assets/logo.svg?component';
import Button from '~/components/atomic/Button.vue';
import useApiClient from '~/compositions/useApiClient';