mirror of
https://github.com/MedzikUser/go-github-selfupdate.git
synced 2024-08-15 03:25:29 +00:00
commit
8a8c4e5c9a
8 changed files with 417 additions and 62 deletions
53
README.md
53
README.md
|
@ -26,6 +26,8 @@ If newer version than itself is detected, it downloads released binary from GitH
|
|||
- Update the binary with rollback support on failure
|
||||
- Tested on Linux, macOS and Windows (using Travis CI and AppVeyor)
|
||||
- Many archive and compression formats are supported (zip, tar, gzip, xzip)
|
||||
- Support private repositories
|
||||
- Support [GitHub Enterprise][]
|
||||
|
||||
And small wrapper CLIs are provided:
|
||||
|
||||
|
@ -81,6 +83,9 @@ It provides `selfupdate` package.
|
|||
- `selfupdate.UpdateCommand()`: Detect the latest version of given repository and update given command.
|
||||
- `selfupdate.DetectLatest()`: Detect the latest version of given repository.
|
||||
- `selfupdate.UpdateTo()`: Update given command to the binary hosted on given URL.
|
||||
- `selfupdate.Updater`: Context manager of self-upadte process. If you want to customize some behavior
|
||||
of self-update (e.g. specify API token, use GitHub Enterprise, ...), please make an instance of
|
||||
`Updater` and use its methods.
|
||||
|
||||
Following is the easiest way to use this package.
|
||||
|
||||
|
@ -154,8 +159,55 @@ func confirmAndSelfUpdate() {
|
|||
}
|
||||
```
|
||||
|
||||
If GitHub API token is set to `[token]` section in `gitconfig` or `$GITHUB_TOKEN` environment variable,
|
||||
this library will use it to call GitHub REST API. It's useful when reaching rate limits or when using
|
||||
this library with private repositories.
|
||||
|
||||
Please see [the documentation page][GoDoc] for more detail.
|
||||
|
||||
This library should work with [GitHub Enterprise][]. To configure API base URL, please setup `Updater`
|
||||
instance and use its method instead (Actually all functions above are just a shortcuts of methods of
|
||||
`Updater` instance).
|
||||
|
||||
Following is an example of usage with GitHub Enterprise.
|
||||
|
||||
```go
|
||||
import (
|
||||
"log"
|
||||
"github.com/blang/semver"
|
||||
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
||||
)
|
||||
|
||||
const version = "1.2.3"
|
||||
|
||||
func doSelfUpdate(token string) {
|
||||
v := semver.MustParse(version)
|
||||
up, err := selfupdate.NewUpdater(selfupdate.Config{
|
||||
APIToken: token,
|
||||
EnterpriseBaseURL: "https://github.your.company.com/api/v3",
|
||||
})
|
||||
latest, err := up.UpdateSelf(v, "myname/myrepo")
|
||||
if err != nil {
|
||||
log.Println("Binary update failed:", err)
|
||||
return
|
||||
}
|
||||
if latest.Version.Equals(v) {
|
||||
// latest version is the same as current version. It means current binary is up to date.
|
||||
log.Println("Current binary is the latest version", version)
|
||||
} else {
|
||||
log.Println("Successfully updated to version", latest.Version)
|
||||
log.Println("Release note:\n", latest.ReleaseNotes)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If `APIToken` field is not given, it tries to retrieve API token from `[token]` section of `.gitconfig`
|
||||
or `$GITHUB_TOKEN` environment variable. If no token is found, it raises an error because GitHub Enterprise
|
||||
API does not work without authentication.
|
||||
|
||||
If your GitHub Enterprise instance's upload URL is different from the base URL, please also set `EnterpriseUploadURL`
|
||||
field
|
||||
|
||||
### Naming Rules of Released Binaries
|
||||
|
||||
go-github-selfupdate assumes that released binaries are put for each combination of platforms and archs.
|
||||
|
@ -279,3 +331,4 @@ Distributed under the [MIT License](LICENSE)
|
|||
[AppVeyor]: https://ci.appveyor.com/project/rhysd/go-github-selfupdate/branch/master
|
||||
[Codecov Status]: https://codecov.io/gh/rhysd/go-github-selfupdate/branch/master/graph/badge.svg
|
||||
[Codecov]: https://codecov.io/gh/rhysd/go-github-selfupdate
|
||||
[GitHub Enterprise]: https://enterprise.github.com/home
|
||||
|
|
|
@ -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 {
|
||||
|
@ -121,10 +92,13 @@ func (d *ReleaseDetector) DetectLatest(slug string) (release *Release, found boo
|
|||
release = &Release{
|
||||
AssetURL: url,
|
||||
AssetByteSize: asset.GetSize(),
|
||||
AssetID: asset.GetID(),
|
||||
URL: rel.GetHTMLURL(),
|
||||
ReleaseNotes: rel.GetBody(),
|
||||
Name: rel.GetName(),
|
||||
PublishedAt: &publishedAt,
|
||||
RepoOwner: repo[0],
|
||||
RepoName: repo[1],
|
||||
}
|
||||
|
||||
release.Version, err = semver.Make(tag)
|
||||
|
@ -133,5 +107,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 DefaultUpdater().DetectLatest(slug)
|
||||
}
|
||||
|
|
|
@ -8,14 +8,6 @@ import (
|
|||
"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 {
|
||||
|
@ -45,9 +37,18 @@ func TestDetectReleaseWithVersionPrefix(t *testing.T) {
|
|||
if r.AssetByteSize == 0 {
|
||||
t.Error("Asset's size is unexpectedly zero")
|
||||
}
|
||||
if r.AssetID == 0 {
|
||||
t.Error("Asset's ID is unexpectedly zero")
|
||||
}
|
||||
if r.PublishedAt.IsZero() {
|
||||
t.Error("Release time is unexpectedly zero")
|
||||
}
|
||||
if r.RepoOwner != "rhysd" {
|
||||
t.Error("Repo owner is not correct:", r.RepoOwner)
|
||||
}
|
||||
if r.RepoName != "github-clone-all" {
|
||||
t.Error("Repo name was not properly detectd:", r.RepoName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectReleasesForVariousArchives(t *testing.T) {
|
||||
|
@ -91,9 +92,18 @@ func TestDetectReleasesForVariousArchives(t *testing.T) {
|
|||
if r.AssetByteSize == 0 {
|
||||
t.Error("Asset's size is unexpectedly zero")
|
||||
}
|
||||
if r.AssetID == 0 {
|
||||
t.Error("Asset's ID is unexpectedly zero")
|
||||
}
|
||||
if r.PublishedAt.IsZero() {
|
||||
t.Error("Release time is unexpectedly zero")
|
||||
}
|
||||
if r.RepoOwner != "rhysd-test" {
|
||||
t.Error("Repo owner should be rhysd-test:", r.RepoOwner)
|
||||
}
|
||||
if !strings.HasPrefix(r.RepoName, "test-release-") {
|
||||
t.Error("Repo name was not properly detectd:", r.RepoName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -119,7 +129,7 @@ func TestDetectNoRelease(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInvalidSlug(t *testing.T) {
|
||||
d := NewDetector()
|
||||
up := DefaultUpdater()
|
||||
|
||||
for _, slug := range []string{
|
||||
"foo",
|
||||
|
@ -128,7 +138,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 +149,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 +159,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)
|
||||
}
|
||||
|
@ -159,3 +167,48 @@ func TestNoReleaseFound(t *testing.T) {
|
|||
t.Fatal("Repo having no release should not be found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFromBrokenGitHubEnterpriseURL(t *testing.T) {
|
||||
up, err := NewUpdater(Config{APIToken: "hogehoge", EnterpriseBaseURL: "https://example.com"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, ok, _ := up.DetectLatest("foo/bar")
|
||||
if ok {
|
||||
t.Fatal("Invalid GitHub Enterprise base URL should raise an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFromGitHubEnterpriseRepo(t *testing.T) {
|
||||
token := os.Getenv("GITHUB_ENTERPRISE_TOKEN")
|
||||
base := os.Getenv("GITHUB_ENTERPRISE_BASE_URL")
|
||||
repo := os.Getenv("GITHUB_ENTERPRISE_REPO")
|
||||
if token == "" {
|
||||
t.Skip("because token for GHE is not found")
|
||||
}
|
||||
if base == "" {
|
||||
t.Skip("because base URL for GHE is not found")
|
||||
}
|
||||
if repo == "" {
|
||||
t.Skip("because repo slug for GHE is not found")
|
||||
}
|
||||
|
||||
up, err := NewUpdater(Config{APIToken: token, EnterpriseBaseURL: base})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r, ok, err := up.DetectLatest(repo)
|
||||
if err != nil {
|
||||
t.Fatal("Fetch failed:", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal(repo, "not found")
|
||||
}
|
||||
if r == nil {
|
||||
t.Fatal("Release not detected")
|
||||
}
|
||||
if !r.Version.Equals(semver.MustParse("1.2.3")) {
|
||||
t.Error("")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ type Release struct {
|
|||
AssetURL string
|
||||
// AssetSize represents the size of asset in bytes
|
||||
AssetByteSize int
|
||||
// AssetID is the ID of the asset on GitHub
|
||||
AssetID int
|
||||
// URL is a URL to release page for browsing
|
||||
URL string
|
||||
// ReleaseNotes is a release notes of the release
|
||||
|
@ -21,4 +23,8 @@ type Release struct {
|
|||
Name string
|
||||
// PublishedAt is the time when the release was published
|
||||
PublishedAt *time.Time
|
||||
// RepoOwner is the owner of the repository of the release
|
||||
RepoOwner string
|
||||
// RepoName is the name of the repository of the release
|
||||
RepoName string
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package selfupdate
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -12,20 +13,11 @@ import (
|
|||
"github.com/inconshreveable/go-update"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
res, err := http.Get(assetURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to download a release file from %s: %s", assetURL, err)
|
||||
}
|
||||
func uncompressAndUpdate(src io.ReadCloser, assetURL, cmdPath string) error {
|
||||
defer src.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("Failed to download a release file from %s", assetURL)
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
_, cmd := filepath.Split(cmdPath)
|
||||
asset, err := UncompressCommand(res.Body, assetURL, cmd)
|
||||
asset, err := UncompressCommand(src, assetURL, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -36,9 +28,51 @@ func UpdateTo(assetURL, cmdPath string) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (up *Updater) downloadDirectlyFromURL(assetURL string) (io.ReadCloser, error) {
|
||||
req, err := http.NewRequest("GET", assetURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to create HTTP request to %s: %s", assetURL, err)
|
||||
}
|
||||
|
||||
req.Header.Add("Accept", "application/octet-stream")
|
||||
req = req.WithContext(up.apiCtx)
|
||||
|
||||
// OAuth HTTP client is not available to download blob from URL when the URL is a redirect URL
|
||||
// returned from GitHub Releases API (response status 400).
|
||||
// Use default HTTP client instead.
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to download a release file from %s: %s", assetURL, err)
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("Failed to download a release file from %s: Not successful status %d", assetURL, res.StatusCode)
|
||||
}
|
||||
|
||||
return res.Body, nil
|
||||
}
|
||||
|
||||
// UpdateTo downloads an executable from GitHub Releases API and replace current binary with the downloaded one.
|
||||
// It downloads a release asset via GitHub Releases API so this function is available for update releases on private repository.
|
||||
// If a redirect occurs, it fallbacks into directly downloading from the redirect URL.
|
||||
func (up *Updater) UpdateTo(rel *Release, cmdPath string) error {
|
||||
src, redirectURL, err := up.api.Repositories.DownloadReleaseAsset(up.apiCtx, rel.RepoOwner, rel.RepoName, rel.AssetID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to call GitHub Releases API for getting an asset(ID: %d) for repository '%s/%s': %s", rel.AssetID, rel.RepoOwner, rel.RepoName, err)
|
||||
}
|
||||
if redirectURL != "" {
|
||||
log.Println("Redirect URL was returned while trying to download a release asset from GitHub API. Falling back to downloading from asset URL directly:", redirectURL)
|
||||
src, err = up.downloadDirectlyFromURL(redirectURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return uncompressAndUpdate(src, rel.AssetURL, cmdPath)
|
||||
}
|
||||
|
||||
// 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 +90,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 +103,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, cmdPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rel, nil
|
||||
|
@ -77,10 +111,35 @@ 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)
|
||||
}
|
||||
|
||||
// UpdateTo downloads an executable from assetURL and replace the current binary with the downloaded one.
|
||||
// This function is low-level API to update the binary. Because it does not use GitHub API and downloads asset directly from the URL via HTTP,
|
||||
// this function is not available to update a release for private repositories.
|
||||
// cmdPath is a file path to command executable.
|
||||
func UpdateTo(assetURL, cmdPath string) error {
|
||||
up := DefaultUpdater()
|
||||
src, err := up.downloadDirectlyFromURL(assetURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return uncompressAndUpdate(src, assetURL, cmdPath)
|
||||
}
|
||||
|
||||
// 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 DefaultUpdater().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 DefaultUpdater().UpdateSelf(current, slug)
|
||||
}
|
||||
|
|
|
@ -256,3 +256,61 @@ func TestBrokenAsset(t *testing.T) {
|
|||
t.Fatal("Unexpected error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrokenGitHubEnterpriseURL(t *testing.T) {
|
||||
up, err := NewUpdater(Config{APIToken: "hogehoge", EnterpriseBaseURL: "https://example.com"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = up.UpdateTo(&Release{AssetURL: "https://example.com"}, "foo")
|
||||
if err == nil {
|
||||
t.Fatal("Invalid GitHub Enterprise base URL should raise an error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Failed to call GitHub Releases API for getting an asset") {
|
||||
t.Error("Unexpected error occurred:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFromGitHubEnterprise(t *testing.T) {
|
||||
token := os.Getenv("GITHUB_ENTERPRISE_TOKEN")
|
||||
base := os.Getenv("GITHUB_ENTERPRISE_BASE_URL")
|
||||
repo := os.Getenv("GITHUB_ENTERPRISE_REPO")
|
||||
if token == "" {
|
||||
t.Skip("because token for GHE is not found")
|
||||
}
|
||||
if base == "" {
|
||||
t.Skip("because base URL for GHE is not found")
|
||||
}
|
||||
if repo == "" {
|
||||
t.Skip("because repo slug for GHE is not found")
|
||||
}
|
||||
|
||||
setupTestBinary()
|
||||
defer teardownTestBinary()
|
||||
|
||||
up, err := NewUpdater(Config{APIToken: token, EnterpriseBaseURL: base})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
latest := semver.MustParse("1.2.3")
|
||||
prev := semver.MustParse("1.2.2")
|
||||
rel, err := up.UpdateCommand("github-release-test", prev, repo)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if rel.Version.NE(latest) {
|
||||
t.Error("Version is not latest", rel.Version)
|
||||
}
|
||||
|
||||
bytes, err := exec.Command(filepath.FromSlash("./github-release-test")).Output()
|
||||
if err != nil {
|
||||
t.Fatal("Failed to run test binary after update:", err)
|
||||
}
|
||||
|
||||
out := string(bytes)
|
||||
if out != "v1.2.3\n" {
|
||||
t.Error("Output from test binary after update is unexpected:", out)
|
||||
}
|
||||
}
|
||||
|
|
85
selfupdate/updater.go
Normal file
85
selfupdate/updater.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package selfupdate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// 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
|
||||
// EnterpriseBaseURL is a base URL of GitHub API. If you want to use this library with GitHub Enterprise,
|
||||
// please set "https://{your-organization-address}/api/v3/" to this field.
|
||||
EnterpriseBaseURL string
|
||||
// EnterpriseUploadURL is a URL to upload stuffs to GitHub Enterprise instance. This is often the same as an API base URL.
|
||||
// So if this field is not set and EnterpriseBaseURL is set, EnterpriseBaseURL is also set to this field.
|
||||
EnterpriseUploadURL string
|
||||
}
|
||||
|
||||
func newHTTPClient(ctx context.Context, token string) *http.Client {
|
||||
if token == "" {
|
||||
return http.DefaultClient
|
||||
}
|
||||
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
|
||||
return oauth2.NewClient(ctx, src)
|
||||
}
|
||||
|
||||
// NewUpdater creates a new updater instance. It initializes GitHub API client.
|
||||
// If you set your API token to $GITHUB_TOKEN, the client will use it.
|
||||
func NewUpdater(config Config) (*Updater, error) {
|
||||
token := config.APIToken
|
||||
if token == "" {
|
||||
token = os.Getenv("GITHUB_TOKEN")
|
||||
}
|
||||
if token == "" {
|
||||
token, _ = gitconfig.GithubToken()
|
||||
}
|
||||
ctx := context.Background()
|
||||
hc := newHTTPClient(ctx, token)
|
||||
|
||||
if config.EnterpriseBaseURL == "" {
|
||||
client := github.NewClient(hc)
|
||||
return &Updater{client, ctx, hc}, nil
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return nil, errors.New("GitHub API token cannot be empty when releases are hosted on GitHub Enterprise instance")
|
||||
}
|
||||
u := config.EnterpriseUploadURL
|
||||
if u == "" {
|
||||
u = config.EnterpriseBaseURL
|
||||
}
|
||||
client, err := github.NewEnterpriseClient(config.EnterpriseBaseURL, u, hc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Updater{client, ctx, hc}, nil
|
||||
}
|
||||
|
||||
// DefaultUpdater creates a new updater instance with default configuration.
|
||||
// It initializes GitHub API client with default API base URL.
|
||||
// If you set your API token to $GITHUB_TOKEN, the client will use it.
|
||||
func DefaultUpdater() *Updater {
|
||||
token := os.Getenv("GITHUB_TOKEN")
|
||||
if token == "" {
|
||||
token, _ = gitconfig.GithubToken()
|
||||
}
|
||||
ctx := context.Background()
|
||||
client := newHTTPClient(ctx, token)
|
||||
return &Updater{github.NewClient(client), ctx, client}
|
||||
}
|
67
selfupdate/updater_test.go
Normal file
67
selfupdate/updater_test.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
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")
|
||||
}
|
||||
_ = DefaultUpdater()
|
||||
if _, err := NewUpdater(Config{}); err != nil {
|
||||
t.Error("Failed to initialize updater with empty config")
|
||||
}
|
||||
if _, err := NewUpdater(Config{APIToken: token}); err != nil {
|
||||
t.Error("Failed to initialize updater with API token config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubEnterpriseClient(t *testing.T) {
|
||||
url := "https://github.company.com/api/v3/"
|
||||
up, err := NewUpdater(Config{APIToken: "hogehoge", EnterpriseBaseURL: url})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if up.api.BaseURL.String() != url {
|
||||
t.Error("Base URL was set to", up.api.BaseURL, ", want", url)
|
||||
}
|
||||
if up.api.UploadURL.String() != url {
|
||||
t.Error("Upload URL was set to", up.api.UploadURL, ", want", url)
|
||||
}
|
||||
|
||||
url2 := "https://upload.github.company.com/api/v3/"
|
||||
up, err = NewUpdater(Config{
|
||||
APIToken: "hogehoge",
|
||||
EnterpriseBaseURL: url,
|
||||
EnterpriseUploadURL: url2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if up.api.BaseURL.String() != url {
|
||||
t.Error("Base URL was set to", up.api.BaseURL, ", want", url)
|
||||
}
|
||||
if up.api.UploadURL.String() != url2 {
|
||||
t.Error("Upload URL was set to", up.api.UploadURL, ", want", url2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubEnterpriseClientWithoutToken(t *testing.T) {
|
||||
token := os.Getenv("GITHUB_TOKEN")
|
||||
defer os.Setenv("GITHUB_TOKEN", token)
|
||||
os.Setenv("GITHUB_TOKEN", "")
|
||||
_, err := NewUpdater(Config{EnterpriseBaseURL: "https://github.company.com/api/v3/"})
|
||||
if err == nil {
|
||||
t.Fatal("Error should be reported because of empty token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubEnterpriseClientInvalidURL(t *testing.T) {
|
||||
_, err := NewUpdater(Config{APIToken: "hogehoge", EnterpriseBaseURL: ":this is not a URL"})
|
||||
if err == nil {
|
||||
t.Fatal("Invalid URL should raise an error")
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue