mirror of
https://github.com/MedzikUser/go-github-selfupdate.git
synced 2024-08-15 03:25:29 +00:00
create Updater struct to use persistent GitHub API client also for downloading assets
This commit is contained in:
parent
54df18b98c
commit
fd492d6b01
6 changed files with 89 additions and 54 deletions
|
@ -1,28 +1,17 @@
|
||||||
package selfupdate
|
package selfupdate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/blang/semver"
|
"github.com/blang/semver"
|
||||||
"github.com/google/go-github/github"
|
"github.com/google/go-github/github"
|
||||||
gitconfig "github.com/tcnksm/go-gitconfig"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var reVersion = regexp.MustCompile(`\d+\.\d+\.\d+`)
|
var reVersion = regexp.MustCompile(`\d+\.\d+\.\d+`)
|
||||||
|
|
||||||
// ReleaseDetector is responsible for detecting the latest release using GitHub Releases API.
|
|
||||||
type ReleaseDetector struct {
|
|
||||||
api *github.Client
|
|
||||||
apiCtx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func findSuitableReleaseAndAsset(rels []*github.RepositoryRelease) (*github.RepositoryRelease, *github.ReleaseAsset, bool) {
|
func findSuitableReleaseAndAsset(rels []*github.RepositoryRelease) (*github.RepositoryRelease, *github.ReleaseAsset, bool) {
|
||||||
// Generate candidates
|
// Generate candidates
|
||||||
cs := make([]string, 0, 8)
|
cs := make([]string, 0, 8)
|
||||||
|
@ -64,33 +53,15 @@ func findSuitableReleaseAndAsset(rels []*github.RepositoryRelease) (*github.Repo
|
||||||
return nil, nil, false
|
return nil, nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDetector crates a new detector instance. It initializes GitHub API client.
|
|
||||||
func NewDetector() *ReleaseDetector {
|
|
||||||
token := os.Getenv("GITHUB_TOKEN")
|
|
||||||
if token == "" {
|
|
||||||
token, _ = gitconfig.GithubToken()
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
var auth *http.Client
|
|
||||||
if token != "" {
|
|
||||||
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
|
|
||||||
auth = oauth2.NewClient(ctx, src)
|
|
||||||
}
|
|
||||||
|
|
||||||
client := github.NewClient(auth)
|
|
||||||
return &ReleaseDetector{client, ctx}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DetectLatest tries to get the latest version of the repository on GitHub. 'slug' means 'owner/name' formatted string.
|
// DetectLatest tries to get the latest version of the repository on GitHub. 'slug' means 'owner/name' formatted string.
|
||||||
func (d *ReleaseDetector) DetectLatest(slug string) (release *Release, found bool, err error) {
|
func (up *Updater) DetectLatest(slug string) (release *Release, found bool, err error) {
|
||||||
repo := strings.Split(slug, "/")
|
repo := strings.Split(slug, "/")
|
||||||
if len(repo) != 2 || repo[0] == "" || repo[1] == "" {
|
if len(repo) != 2 || repo[0] == "" || repo[1] == "" {
|
||||||
err = fmt.Errorf("Invalid slug format. It should be 'owner/name': %s", slug)
|
err = fmt.Errorf("Invalid slug format. It should be 'owner/name': %s", slug)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rels, res, err := d.api.Repositories.ListReleases(d.apiCtx, repo[0], repo[1], nil)
|
rels, res, err := up.api.Repositories.ListReleases(up.apiCtx, repo[0], repo[1], nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("API returned an error response:", err)
|
log.Println("API returned an error response:", err)
|
||||||
if res != nil && res.StatusCode == 404 {
|
if res != nil && res.StatusCode == 404 {
|
||||||
|
@ -133,5 +104,5 @@ func (d *ReleaseDetector) DetectLatest(slug string) (release *Release, found boo
|
||||||
|
|
||||||
// DetectLatest detects the latest release of the slug (owner/repo).
|
// DetectLatest detects the latest release of the slug (owner/repo).
|
||||||
func DetectLatest(slug string) (*Release, bool, error) {
|
func DetectLatest(slug string) (*Release, bool, error) {
|
||||||
return NewDetector().DetectLatest(slug)
|
return NewUpdater(Config{}).DetectLatest(slug)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,19 +3,10 @@ package selfupdate
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/blang/semver"
|
"github.com/blang/semver"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGitHubTokenEnv(t *testing.T) {
|
|
||||||
token := os.Getenv("GITHUB_TOKEN")
|
|
||||||
if token == "" {
|
|
||||||
t.Skip("because $GITHUB_TOKEN is not set")
|
|
||||||
}
|
|
||||||
_ = NewDetector()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDetectReleaseWithVersionPrefix(t *testing.T) {
|
func TestDetectReleaseWithVersionPrefix(t *testing.T) {
|
||||||
r, ok, err := DetectLatest("rhysd/github-clone-all")
|
r, ok, err := DetectLatest("rhysd/github-clone-all")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -119,7 +110,7 @@ func TestDetectNoRelease(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInvalidSlug(t *testing.T) {
|
func TestInvalidSlug(t *testing.T) {
|
||||||
d := NewDetector()
|
up := NewUpdater(Config{})
|
||||||
|
|
||||||
for _, slug := range []string{
|
for _, slug := range []string{
|
||||||
"foo",
|
"foo",
|
||||||
|
@ -128,7 +119,7 @@ func TestInvalidSlug(t *testing.T) {
|
||||||
"/bar",
|
"/bar",
|
||||||
"foo/bar/piyo",
|
"foo/bar/piyo",
|
||||||
} {
|
} {
|
||||||
_, _, err := d.DetectLatest(slug)
|
_, _, err := up.DetectLatest(slug)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error(slug, "should be invalid slug")
|
t.Error(slug, "should be invalid slug")
|
||||||
}
|
}
|
||||||
|
@ -139,8 +130,7 @@ func TestInvalidSlug(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNonExistingRepo(t *testing.T) {
|
func TestNonExistingRepo(t *testing.T) {
|
||||||
d := NewDetector()
|
v, ok, err := DetectLatest("rhysd/non-existing-repo")
|
||||||
v, ok, err := d.DetectLatest("rhysd/non-existing-repo")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Non-existing repo should not cause an error:", v)
|
t.Fatal("Non-existing repo should not cause an error:", v)
|
||||||
}
|
}
|
||||||
|
@ -150,8 +140,7 @@ func TestNonExistingRepo(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNoReleaseFound(t *testing.T) {
|
func TestNoReleaseFound(t *testing.T) {
|
||||||
d := NewDetector()
|
_, ok, err := DetectLatest("rhysd/misc")
|
||||||
_, ok, err := d.DetectLatest("rhysd/misc")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Repo having no release should not cause an error:", err)
|
t.Fatal("Repo having no release should not cause an error:", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateTo download an executable from assetURL and replace the current binary with the downloaded one. cmdPath is a file path to command executable.
|
// UpdateTo download an executable from assetURL and replace the current binary with the downloaded one. cmdPath is a file path to command executable.
|
||||||
func UpdateTo(assetURL, cmdPath string) error {
|
func (up *Updater) UpdateTo(assetURL, cmdPath string) error {
|
||||||
|
// TODO: Use GitHub API client to download assets
|
||||||
|
|
||||||
res, err := http.Get(assetURL)
|
res, err := http.Get(assetURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to download a release file from %s: %s", assetURL, err)
|
return fmt.Errorf("Failed to download a release file from %s: %s", assetURL, err)
|
||||||
|
@ -38,7 +40,7 @@ func UpdateTo(assetURL, cmdPath string) error {
|
||||||
|
|
||||||
// UpdateCommand updates a given command binary to the latest version.
|
// UpdateCommand updates a given command binary to the latest version.
|
||||||
// 'slug' represents 'owner/name' repository on GitHub and 'current' means the current version.
|
// 'slug' represents 'owner/name' repository on GitHub and 'current' means the current version.
|
||||||
func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Release, error) {
|
func (up *Updater) UpdateCommand(cmdPath string, current semver.Version, slug string) (*Release, error) {
|
||||||
if runtime.GOOS == "windows" && !strings.HasSuffix(cmdPath, ".exe") {
|
if runtime.GOOS == "windows" && !strings.HasSuffix(cmdPath, ".exe") {
|
||||||
// Ensure to add '.exe' to given path on Windows
|
// Ensure to add '.exe' to given path on Windows
|
||||||
cmdPath = cmdPath + ".exe"
|
cmdPath = cmdPath + ".exe"
|
||||||
|
@ -56,7 +58,7 @@ func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Releas
|
||||||
cmdPath = p
|
cmdPath = p
|
||||||
}
|
}
|
||||||
|
|
||||||
rel, ok, err := DetectLatest(slug)
|
rel, ok, err := up.DetectLatest(slug)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -69,7 +71,7 @@ func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Releas
|
||||||
return rel, nil
|
return rel, nil
|
||||||
}
|
}
|
||||||
log.Println("Will update", cmdPath, "to the latest version", rel.Version)
|
log.Println("Will update", cmdPath, "to the latest version", rel.Version)
|
||||||
if err := UpdateTo(rel.AssetURL, cmdPath); err != nil {
|
if err := up.UpdateTo(rel.AssetURL, cmdPath); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return rel, nil
|
return rel, nil
|
||||||
|
@ -77,10 +79,22 @@ func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Releas
|
||||||
|
|
||||||
// UpdateSelf updates the running executable itself to the latest version.
|
// UpdateSelf updates the running executable itself to the latest version.
|
||||||
// 'slug' represents 'owner/name' repository on GitHub and 'current' means the current version.
|
// 'slug' represents 'owner/name' repository on GitHub and 'current' means the current version.
|
||||||
func UpdateSelf(current semver.Version, slug string) (*Release, error) {
|
func (up *Updater) UpdateSelf(current semver.Version, slug string) (*Release, error) {
|
||||||
cmdPath, err := os.Executable()
|
cmdPath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return UpdateCommand(cmdPath, current, slug)
|
return UpdateCommand(cmdPath, current, slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateCommand updates a given command binary to the latest version.
|
||||||
|
// This function is a shortcut version of updater.UpdateCommand.
|
||||||
|
func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Release, error) {
|
||||||
|
return NewUpdater(Config{}).UpdateCommand(cmdPath, current, slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSelf updates the running executable itself to the latest version.
|
||||||
|
// This function is a shortcut version of updater.UpdateSelf.
|
||||||
|
func UpdateSelf(current semver.Version, slug string) (*Release, error) {
|
||||||
|
return NewUpdater(Config{}).UpdateSelf(current, slug)
|
||||||
|
}
|
||||||
|
|
|
@ -237,7 +237,7 @@ func TestInvalidSlugForUpdate(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInvalidAssetURL(t *testing.T) {
|
func TestInvalidAssetURL(t *testing.T) {
|
||||||
err := UpdateTo("https://github.com/rhysd/non-existing-repo/releases/download/v1.2.3/foo.zip", "foo")
|
err := NewUpdater(Config{}).UpdateTo("https://github.com/rhysd/non-existing-repo/releases/download/v1.2.3/foo.zip", "foo")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("Error should occur for URL not found")
|
t.Fatal("Error should occur for URL not found")
|
||||||
}
|
}
|
||||||
|
@ -248,7 +248,7 @@ func TestInvalidAssetURL(t *testing.T) {
|
||||||
|
|
||||||
func TestBrokenAsset(t *testing.T) {
|
func TestBrokenAsset(t *testing.T) {
|
||||||
asset := "https://github.com/rhysd-test/test-incorrect-release/releases/download/invalid/broken-zip.zip"
|
asset := "https://github.com/rhysd-test/test-incorrect-release/releases/download/invalid/broken-zip.zip"
|
||||||
err := UpdateTo(asset, "foo")
|
err := NewUpdater(Config{}).UpdateTo(asset, "foo")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("Error should occur for URL not found")
|
t.Fatal("Error should occur for URL not found")
|
||||||
}
|
}
|
||||||
|
|
46
selfupdate/updater.go
Normal file
46
selfupdate/updater.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/google/go-github/github"
|
||||||
|
gitconfig "github.com/tcnksm/go-gitconfig"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Updater is responsible for managing the context of self-update.
|
||||||
|
// It contains GitHub client and its context.
|
||||||
|
type Updater struct {
|
||||||
|
api *github.Client
|
||||||
|
apiCtx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config represents the configuration of self-update.
|
||||||
|
type Config struct {
|
||||||
|
// APIToken represents GitHub API token. If it's not empty, it will be used for authentication of GitHub API
|
||||||
|
APIToken string
|
||||||
|
// TODO: Add host URL for API endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUpdater crates a new detector instance. It initializes GitHub API client.
|
||||||
|
func NewUpdater(config Config) *Updater {
|
||||||
|
token := config.APIToken
|
||||||
|
if token == "" {
|
||||||
|
token = os.Getenv("GITHUB_TOKEN")
|
||||||
|
}
|
||||||
|
if token == "" {
|
||||||
|
token, _ = gitconfig.GithubToken()
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var auth *http.Client
|
||||||
|
if token != "" {
|
||||||
|
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
|
||||||
|
auth = oauth2.NewClient(ctx, src)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := github.NewClient(auth)
|
||||||
|
return &Updater{client, ctx}
|
||||||
|
}
|
15
selfupdate/updater_test.go
Normal file
15
selfupdate/updater_test.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGitHubTokenEnv(t *testing.T) {
|
||||||
|
token := os.Getenv("GITHUB_TOKEN")
|
||||||
|
if token == "" {
|
||||||
|
t.Skip("because $GITHUB_TOKEN is not set")
|
||||||
|
}
|
||||||
|
_ = NewUpdater(Config{})
|
||||||
|
_ = NewUpdater(Config{APIToken: token})
|
||||||
|
}
|
Loading…
Reference in a new issue