diff --git a/README.md b/README.md index 88e82de..7c809c6 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ It provides `selfupdate` package. ```go import ( "log" - "github.com/rhysd/go-github-selfupdate" + "github.com/rhysd/go-github-selfupdate/selfupdate" ) func doUpdate(version string) { diff --git a/cmd/selfupdate-example/main.go b/cmd/selfupdate-example/main.go index eb95f10..4fb30f4 100644 --- a/cmd/selfupdate-example/main.go +++ b/cmd/selfupdate-example/main.go @@ -3,13 +3,13 @@ package main import ( "flag" "fmt" + "github.com/rhysd/go-github-selfupdate/selfupdate" "os" ) const version = "1.2.3" func selfUpdate() error { - return nil up, err := selfupdate.TryUpdate(version, "go-github-selfupdate", nil) if err != nil { return err diff --git a/selfupdate/detect.go b/selfupdate/detect.go new file mode 100644 index 0000000..f3bbb65 --- /dev/null +++ b/selfupdate/detect.go @@ -0,0 +1,71 @@ +package selfupdate + +import ( + "context" + "fmt" + "github.com/blang/semver" + "github.com/google/go-github/github" + "golang.org/x/oauth2" + "net/http" + "os" + "strings" +) + +// ReleaseDetector is responsible for detecting the latest release using GitHub Releases API. +type ReleaseDetector struct { + verPrefix string + api *github.Client + apiCtx context.Context +} + +// NewDetector crates a new detector instance. It initializes GitHub API client. +func NewDetector(versionPrefix string) *ReleaseDetector { + token := os.Getenv("GITHUB_TOKEN") + 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{versionPrefix, 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) (ver semver.Version, 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 + } + + rel, res, err := d.api.Repositories.GetLatestRelease(d.apiCtx, repo[0], repo[1]) + if err != nil { + if res.StatusCode == 404 { + // 404 means repository not found or release not found. It's not an error here. + found = false + err = nil + log.Println("API returned 404. Repository or release not found") + } else { + log.Println("API returned an error:", err) + } + return + } + + tag := rel.GetTagName() + log.Println("Successfully fetched the latest release. tag:", tag, ", name:", rel.GetName(), ", URL:", rel.GetURL()) + + ver, err = semver.Make(strings.TrimPrefix(tag, d.verPrefix)) + if err == nil { + found = true + } + + return +} + +// DetectLatest detects the latest release of the slug (owner/repo). verPrefix is a prefix of version in tag name (i.e. 'v' for 'v1.2.3'). +func DetectLatest(slug, verPrefix string) (semver.Version, bool, error) { + return NewDetector(verPrefix).DetectLatest(slug) +} diff --git a/selfupdate/detect_test.go b/selfupdate/detect_test.go new file mode 100644 index 0000000..7860433 --- /dev/null +++ b/selfupdate/detect_test.go @@ -0,0 +1,67 @@ +package selfupdate + +import ( + "github.com/blang/semver" + "os" + "testing" +) + +func TestGitHubTokenEnv(t *testing.T) { + token := os.Getenv("GITHUB_TOKEN") + if token == "" { + t.Skip("because $GITHUB_TOKEN is not set") + } + _ = NewDetector("") +} + +func TestDetectRelease(t *testing.T) { + v, ok, err := DetectLatest("rhysd/github-clone-all", "v") + if err != nil { + t.Fatal("Fetch failed:", err) + } + if !ok { + t.Fatal("Failed to detect latest") + } + if v.LE(semver.MustParse("2.0.0")) { + t.Fatal("Incorrect version:", v) + } +} + +func TestInvalidSlug(t *testing.T) { + d := NewDetector("v") + + for _, slug := range []string{ + "foo", + "/", + "foo/", + "/bar", + "foo/bar/piyo", + } { + _, _, err := d.DetectLatest(slug) + if err == nil { + t.Error(slug, "should be invalid slug") + } + } +} + +func TestNonExistingRepo(t *testing.T) { + d := NewDetector("") + v, ok, err := d.DetectLatest("rhysd/non-existing-repo") + if err != nil { + t.Fatal("Non-existing repo should not cause an error:", v) + } + if ok { + t.Fatal("Release for non-existing repo should not be found") + } +} + +func TestNoReleaseFound(t *testing.T) { + d := NewDetector("") + _, ok, err := d.DetectLatest("rhysd/misc") + if err != nil { + t.Fatal("Repo having no release should not cause an error:", err) + } + if ok { + t.Fatal("Repo having no release should not be found") + } +} diff --git a/selfupdate/log.go b/selfupdate/log.go new file mode 100644 index 0000000..82dfebe --- /dev/null +++ b/selfupdate/log.go @@ -0,0 +1,28 @@ +package selfupdate + +import ( + "io/ioutil" + stdlog "log" + "os" +) + +var log = stdlog.New(os.Stderr, "", stdlog.Ltime) +var logEnabled = true + +// EnableLog enables to output logging messages in library +func EnableLog() { + if logEnabled { + return + } + logEnabled = true + log.SetOutput(os.Stderr) +} + +// DisableLog disables to output logging messages in library +func DisableLog() { + if !logEnabled { + return + } + logEnabled = false + log.SetOutput(ioutil.Discard) +} diff --git a/selfupdate/log_test.go b/selfupdate/log_test.go new file mode 100644 index 0000000..91dd600 --- /dev/null +++ b/selfupdate/log_test.go @@ -0,0 +1,30 @@ +package selfupdate + +import ( + "testing" +) + +func TestEnableDisableLog(t *testing.T) { + defer EnableLog() + + EnableLog() + if !logEnabled { + t.Fatal("Log should be enabled") + } + EnableLog() + if !logEnabled { + t.Fatal("Log should be enabled") + } + DisableLog() + if logEnabled { + t.Fatal("Log should not be enabled") + } + DisableLog() + if logEnabled { + t.Fatal("Log should not be enabled") + } + EnableLog() + if !logEnabled { + t.Fatal("Log should be enabled") + } +}