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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/google/go-github/github"
|
||||
gitconfig "github.com/tcnksm/go-gitconfig"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
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) {
|
||||
// Generate candidates
|
||||
cs := make([]string, 0, 8)
|
||||
|
@ -64,33 +53,15 @@ func findSuitableReleaseAndAsset(rels []*github.RepositoryRelease) (*github.Repo
|
|||
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.
|
||||
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, "/")
|
||||
if len(repo) != 2 || repo[0] == "" || repo[1] == "" {
|
||||
err = fmt.Errorf("Invalid slug format. It should be 'owner/name': %s", slug)
|
||||
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 {
|
||||
log.Println("API returned an error response:", err)
|
||||
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).
|
||||
func DetectLatest(slug string) (*Release, bool, error) {
|
||||
return NewDetector().DetectLatest(slug)
|
||||
return NewUpdater(Config{}).DetectLatest(slug)
|
||||
}
|
||||
|
|
|
@ -3,19 +3,10 @@ package selfupdate
|
|||
import (
|
||||
"fmt"
|
||||
"github.com/blang/semver"
|
||||
"os"
|
||||
"strings"
|
||||
"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) {
|
||||
r, ok, err := DetectLatest("rhysd/github-clone-all")
|
||||
if err != nil {
|
||||
|
@ -119,7 +110,7 @@ func TestDetectNoRelease(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInvalidSlug(t *testing.T) {
|
||||
d := NewDetector()
|
||||
up := NewUpdater(Config{})
|
||||
|
||||
for _, slug := range []string{
|
||||
"foo",
|
||||
|
@ -128,7 +119,7 @@ func TestInvalidSlug(t *testing.T) {
|
|||
"/bar",
|
||||
"foo/bar/piyo",
|
||||
} {
|
||||
_, _, err := d.DetectLatest(slug)
|
||||
_, _, err := up.DetectLatest(slug)
|
||||
if err == nil {
|
||||
t.Error(slug, "should be invalid slug")
|
||||
}
|
||||
|
@ -139,8 +130,7 @@ func TestInvalidSlug(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNonExistingRepo(t *testing.T) {
|
||||
d := NewDetector()
|
||||
v, ok, err := d.DetectLatest("rhysd/non-existing-repo")
|
||||
v, ok, err := DetectLatest("rhysd/non-existing-repo")
|
||||
if err != nil {
|
||||
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) {
|
||||
d := NewDetector()
|
||||
_, ok, err := d.DetectLatest("rhysd/misc")
|
||||
_, ok, err := DetectLatest("rhysd/misc")
|
||||
if err != nil {
|
||||
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.
|
||||
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)
|
||||
if err != nil {
|
||||
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.
|
||||
// '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") {
|
||||
// Ensure to add '.exe' to given path on Windows
|
||||
cmdPath = cmdPath + ".exe"
|
||||
|
@ -56,7 +58,7 @@ func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Releas
|
|||
cmdPath = p
|
||||
}
|
||||
|
||||
rel, ok, err := DetectLatest(slug)
|
||||
rel, ok, err := up.DetectLatest(slug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -69,7 +71,7 @@ func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Releas
|
|||
return rel, nil
|
||||
}
|
||||
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 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.
|
||||
// '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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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) {
|
||||
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 {
|
||||
t.Fatal("Error should occur for URL not found")
|
||||
}
|
||||
|
@ -248,7 +248,7 @@ func TestInvalidAssetURL(t *testing.T) {
|
|||
|
||||
func TestBrokenAsset(t *testing.T) {
|
||||
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 {
|
||||
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