Cli updater (#3382)

This commit is contained in:
Anbraten
2024-02-19 09:16:27 +01:00
committed by GitHub
parent 99037b2d97
commit 30b92edc99
7 changed files with 361 additions and 2 deletions

72
cli/update/command.go Normal file
View 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
View 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
View 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
View 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
}