mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-23 00:46:30 +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"},
|
||||
Usage: "server address",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
EnvVars: []string{"DISABLE_UPDATE_CHECK"},
|
||||
Name: "disable-update-check",
|
||||
Usage: "disable update check",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
EnvVars: []string{"WOODPECKER_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/repo"
|
||||
"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/version"
|
||||
)
|
||||
|
@ -37,11 +38,13 @@ import (
|
|||
func newApp() *cli.App {
|
||||
app := cli.NewApp()
|
||||
app.Name = "woodpecker-cli"
|
||||
app.Description = "Woodpecker command line utility"
|
||||
app.Version = version.String()
|
||||
app.Usage = "command line utility"
|
||||
app.EnableBashCompletion = true
|
||||
app.Flags = common.GlobalFlags
|
||||
app.Before = common.SetupGlobalLogger
|
||||
app.Before = common.Before
|
||||
app.After = common.After
|
||||
app.Suggest = true
|
||||
app.Commands = []*cli.Command{
|
||||
pipeline.Command,
|
||||
log.Command,
|
||||
|
@ -55,6 +58,7 @@ func newApp() *cli.App {
|
|||
lint.Command,
|
||||
loglevel.Command,
|
||||
cron.Command,
|
||||
update.Command,
|
||||
}
|
||||
|
||||
return app
|
||||
|
|
Loading…
Reference in a new issue