mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-22 18:01:02 +00:00
Cli updater (#3382)
This commit is contained in:
parent
99037b2d97
commit
30b92edc99
7 changed files with 361 additions and 2 deletions
|
@ -33,6 +33,11 @@ var GlobalFlags = append([]cli.Flag{
|
||||||
Aliases: []string{"s"},
|
Aliases: []string{"s"},
|
||||||
Usage: "server address",
|
Usage: "server address",
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
EnvVars: []string{"DISABLE_UPDATE_CHECK"},
|
||||||
|
Name: "disable-update-check",
|
||||||
|
Usage: "disable update check",
|
||||||
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
EnvVars: []string{"WOODPECKER_SKIP_VERIFY"},
|
EnvVars: []string{"WOODPECKER_SKIP_VERIFY"},
|
||||||
Name: "skip-verify",
|
Name: "skip-verify",
|
||||||
|
|
67
cli/common/hooks.go
Normal file
67
cli/common/hooks.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v2/cli/update"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
waitForUpdateCheck context.Context
|
||||||
|
cancelWaitForUpdate context.CancelCauseFunc
|
||||||
|
)
|
||||||
|
|
||||||
|
func Before(c *cli.Context) error {
|
||||||
|
if err := SetupGlobalLogger(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if c.Bool("disable-update-check") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't check for updates when the update command is executed
|
||||||
|
if firstArg := c.Args().First(); firstArg == "update" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForUpdateCheck, cancelWaitForUpdate = context.WithCancelCause(context.Background())
|
||||||
|
defer cancelWaitForUpdate(errors.New("update check finished"))
|
||||||
|
|
||||||
|
log.Debug().Msg("Checking for updates ...")
|
||||||
|
|
||||||
|
newVersion, err := update.CheckForUpdate(waitForUpdateCheck, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Failed to check for updates")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if newVersion != nil {
|
||||||
|
log.Warn().Msgf("A new version of woodpecker-cli is available: %s. Update by running: %s update", newVersion.Version, c.App.Name)
|
||||||
|
} else {
|
||||||
|
log.Debug().Msgf("No update required")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func After(_ *cli.Context) error {
|
||||||
|
if waitForUpdateCheck != nil {
|
||||||
|
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):
|
||||||
|
log.Debug().Msg("Update check stopped due to timeout")
|
||||||
|
cancelWaitForUpdate(errors.New("update check timeout"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
72
cli/update/command.go
Normal file
72
cli/update/command.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command exports the update command.
|
||||||
|
var Command = &cli.Command{
|
||||||
|
Name: "update",
|
||||||
|
Usage: "update the woodpecker-cli to the latest version",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "force",
|
||||||
|
Usage: "force update even if the latest version is already installed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: update,
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(c *cli.Context) error {
|
||||||
|
log.Info().Msg("Checking for updates ...")
|
||||||
|
|
||||||
|
newVersion, err := CheckForUpdate(c.Context, c.Bool("force"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if newVersion == nil {
|
||||||
|
fmt.Println("You are using the latest version of woodpecker-cli")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msgf("New version %s is available! Updating ...", newVersion.Version)
|
||||||
|
|
||||||
|
var tarFilePath string
|
||||||
|
tarFilePath, err = downloadNewVersion(c.Context, newVersion.AssetURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("New version %s has been downloaded successfully! Installing ...", newVersion.Version)
|
||||||
|
|
||||||
|
binFile, err := extractNewVersion(tarFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("New version %s has been extracted to %s", newVersion.Version, binFile)
|
||||||
|
|
||||||
|
executablePathOrSymlink, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
executablePath, err := filepath.EvalSymlinks(executablePathOrSymlink)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(binFile, executablePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msgf("woodpecker-cli has been updated to version %s successfully!", newVersion.Version)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
60
cli/update/tar.go
Normal file
60
cli/update/tar.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tarDirectoryMode fs.FileMode = 0x755
|
||||||
|
|
||||||
|
func Untar(dst string, r io.Reader) error {
|
||||||
|
gzr, err := gzip.NewReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer gzr.Close()
|
||||||
|
|
||||||
|
tr := tar.NewReader(gzr)
|
||||||
|
|
||||||
|
for {
|
||||||
|
header, err := tr.Next()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == io.EOF:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case err != nil:
|
||||||
|
return err
|
||||||
|
|
||||||
|
case header == nil:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
target := filepath.Join(dst, header.Name)
|
||||||
|
|
||||||
|
switch header.Typeflag {
|
||||||
|
case tar.TypeDir:
|
||||||
|
if _, err := os.Stat(target); err != nil {
|
||||||
|
if err := os.MkdirAll(target, tarDirectoryMode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case tar.TypeReg:
|
||||||
|
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(f, tr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
cli/update/types.go
Normal file
16
cli/update/types.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
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 NewVersion struct {
|
||||||
|
Version string
|
||||||
|
AssetURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
const githubReleaseURL = "https://api.github.com/repos/woodpecker-ci/woodpecker/releases/latest"
|
135
cli/update/updater.go
Normal file
135
cli/update/updater.go
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v2/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CheckForUpdate(ctx context.Context, 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")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", githubReleaseURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, errors.New("failed to fetch the latest release")
|
||||||
|
}
|
||||||
|
|
||||||
|
var release GithubRelease
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// using the latest release
|
||||||
|
if release.TagName == version.String() && !force {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &NewVersion{
|
||||||
|
Version: release.TagName,
|
||||||
|
AssetURL: assetURL,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadNewVersion(ctx context.Context, downloadURL string) (string, error) {
|
||||||
|
log.Debug().Msgf("Downloading new version from %s ...", downloadURL)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", errors.New("failed to download the new version")
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.CreateTemp("", "woodpecker-cli-*.tar.gz")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(file, resp.Body); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("New version downloaded to %s", file.Name())
|
||||||
|
|
||||||
|
return file.Name(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractNewVersion(tarFilePath string) (string, error) {
|
||||||
|
log.Debug().Msgf("Extracting new version from %s ...", tarFilePath)
|
||||||
|
|
||||||
|
tarFile, err := os.Open(tarFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tarFile.Close()
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("", "woodpecker-cli-*")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = Untar(tmpDir, tarFile)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Remove(tarFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("New version extracted to %s", tmpDir)
|
||||||
|
|
||||||
|
return path.Join(tmpDir, "woodpecker-cli"), nil
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ import (
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/cli/registry"
|
"go.woodpecker-ci.org/woodpecker/v2/cli/registry"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/cli/repo"
|
"go.woodpecker-ci.org/woodpecker/v2/cli/repo"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/cli/secret"
|
"go.woodpecker-ci.org/woodpecker/v2/cli/secret"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v2/cli/update"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/cli/user"
|
"go.woodpecker-ci.org/woodpecker/v2/cli/user"
|
||||||
"go.woodpecker-ci.org/woodpecker/v2/version"
|
"go.woodpecker-ci.org/woodpecker/v2/version"
|
||||||
)
|
)
|
||||||
|
@ -37,11 +38,13 @@ import (
|
||||||
func newApp() *cli.App {
|
func newApp() *cli.App {
|
||||||
app := cli.NewApp()
|
app := cli.NewApp()
|
||||||
app.Name = "woodpecker-cli"
|
app.Name = "woodpecker-cli"
|
||||||
|
app.Description = "Woodpecker command line utility"
|
||||||
app.Version = version.String()
|
app.Version = version.String()
|
||||||
app.Usage = "command line utility"
|
|
||||||
app.EnableBashCompletion = true
|
app.EnableBashCompletion = true
|
||||||
app.Flags = common.GlobalFlags
|
app.Flags = common.GlobalFlags
|
||||||
app.Before = common.SetupGlobalLogger
|
app.Before = common.Before
|
||||||
|
app.After = common.After
|
||||||
|
app.Suggest = true
|
||||||
app.Commands = []*cli.Command{
|
app.Commands = []*cli.Command{
|
||||||
pipeline.Command,
|
pipeline.Command,
|
||||||
log.Command,
|
log.Command,
|
||||||
|
@ -55,6 +58,7 @@ func newApp() *cli.App {
|
||||||
lint.Command,
|
lint.Command,
|
||||||
loglevel.Command,
|
loglevel.Command,
|
||||||
cron.Command,
|
cron.Command,
|
||||||
|
update.Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
Loading…
Reference in a new issue