Adding upstream version 1.2.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
3a3aa427d7
commit
e7ed09875d
58 changed files with 3068 additions and 0 deletions
29
.appveyor.yml
Normal file
29
.appveyor.yml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
version: "{build}"
|
||||||
|
clone_depth: 1
|
||||||
|
clone_folder: c:\outside-gopath
|
||||||
|
environment:
|
||||||
|
GOPATH: c:\gopath
|
||||||
|
GO111MODULE: on
|
||||||
|
install:
|
||||||
|
- echo %PATH%
|
||||||
|
- echo %GOPATH%
|
||||||
|
- go version
|
||||||
|
- go env
|
||||||
|
- go get -v -t -d ./...
|
||||||
|
build: off
|
||||||
|
test_script:
|
||||||
|
- go build ./selfupdate
|
||||||
|
- go build ./cmd/selfupdate-example
|
||||||
|
- go build ./cmd/detect-latest-release
|
||||||
|
- go build ./cmd/go-get-release/
|
||||||
|
- ps: |
|
||||||
|
if (Test-Path env:GITHUB_TOKEN) {
|
||||||
|
go test -v -race "-coverprofile=coverage.txt" ./selfupdate
|
||||||
|
} else {
|
||||||
|
go test -v -race -short ./selfupdate
|
||||||
|
}
|
||||||
|
after_test:
|
||||||
|
- "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%"
|
||||||
|
- pip install codecov
|
||||||
|
- codecov -f "coverage.txt"
|
||||||
|
deploy: off
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/selfupdate-example
|
||||||
|
/release
|
||||||
|
/env.sh
|
||||||
|
/detect-latest-release
|
||||||
|
/go-get-release
|
||||||
|
/coverage.out
|
23
.travis.yml
Normal file
23
.travis.yml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
language: go
|
||||||
|
os:
|
||||||
|
- linux
|
||||||
|
- osx
|
||||||
|
env:
|
||||||
|
- GO111MODULE=on
|
||||||
|
install:
|
||||||
|
- go version
|
||||||
|
- go env
|
||||||
|
- go get -t -d -v ./...
|
||||||
|
script:
|
||||||
|
- go build ./selfupdate/
|
||||||
|
- go build ./cmd/selfupdate-example/
|
||||||
|
- go build ./cmd/detect-latest-release/
|
||||||
|
- go build ./cmd/go-get-release/
|
||||||
|
- |
|
||||||
|
if [[ "${GITHUB_TOKEN}" != "" ]]; then
|
||||||
|
go test -v -race -coverprofile=coverage.txt ./selfupdate
|
||||||
|
else
|
||||||
|
go test -v -race -short ./selfupdate
|
||||||
|
fi
|
||||||
|
after_success:
|
||||||
|
- if [ -f coverage.txt ]; then bash <(curl -s https://codecov.io/bash); fi
|
40
CHANGELOG.md
Normal file
40
CHANGELOG.md
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
## [v1.2.3] - 2021-01-13
|
||||||
|
|
||||||
|
- Fix security issues in dependencies; CVE-2020-16845, CVE-2019-11840, CVE-2020-14040 (Thanks to [@bhamail](https://github.com/bhamail)).
|
||||||
|
|
||||||
|
## [v1.2.2] - 2020-04-10
|
||||||
|
|
||||||
|
- Update `go-github` dependency to v30.1.0
|
||||||
|
|
||||||
|
## [v1.2.1] - 2019-12-19
|
||||||
|
|
||||||
|
- Fix `.tgz` file was not handled as `.tar.gz`.
|
||||||
|
|
||||||
|
|
||||||
|
## [v1.2.0] - 2019-12-19
|
||||||
|
|
||||||
|
- New Feature: Filtering releases by matching regular expressions to release names (Thanks to [@fredbi](https://github.com/fredbi)).
|
||||||
|
Regular expression strings specified at `Filters` field in `Config` struct are used on detecting the
|
||||||
|
latest release. Please read [documentation](https://godoc.org/github.com/rhysd/go-github-selfupdate/selfupdate#Config)
|
||||||
|
for more details.
|
||||||
|
- Allow `{cmd}_{os}_{arch}` format for executable names.
|
||||||
|
- `.tgz` file name suffix was supported.
|
||||||
|
|
||||||
|
|
||||||
|
## [v1.1.0] - 2018-11-10
|
||||||
|
|
||||||
|
- New Feature: Signature validation for release assets (Thanks to [@tobiaskohlbau](https://github.com/tobiaskohlbau)).
|
||||||
|
Please read [the instruction](https://github.com/rhysd/go-github-selfupdate#hash-or-signature-validation) for usage.
|
||||||
|
|
||||||
|
|
||||||
|
## [v1.0.0] - 2018-09-23
|
||||||
|
|
||||||
|
First release! :tada:
|
||||||
|
|
||||||
|
|
||||||
|
[v1.2.3]: https://github.com/rhysd/go-github-selfupdate/compare/v1.2.2...v1.2.3
|
||||||
|
[v1.2.2]: https://github.com/rhysd/go-github-selfupdate/compare/v1.2.1...v1.2.2
|
||||||
|
[v1.2.1]: https://github.com/rhysd/go-github-selfupdate/compare/v1.2.0...v1.2.1
|
||||||
|
[v1.2.0]: https://github.com/rhysd/go-github-selfupdate/compare/go-get-release...v1.2.0
|
||||||
|
[v1.1.0]: https://github.com/rhysd/go-github-selfupdate/compare/v1.0.0...v1.1.0
|
||||||
|
[v1.0.0]: https://github.com/rhysd/go-github-selfupdate/compare/example-1.2.4...v1.0.0
|
18
Guardfile
Normal file
18
Guardfile
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
guard :shell do
|
||||||
|
watch /^selfupdate\/.+\.go$/ do |m|
|
||||||
|
puts "#{Time.now}: #{m[0]}"
|
||||||
|
case m[0]
|
||||||
|
when /_test\.go$/
|
||||||
|
parent = File.dirname m[0]
|
||||||
|
sources = Dir["#{parent}/*.go"].reject{|p| p.end_with? '_test.go'}.join(' ')
|
||||||
|
system "go test -v -short #{m[0]} #{sources}"
|
||||||
|
else
|
||||||
|
system 'go build ./selfupdate/'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
watch /^cmd\/selfupdate-example\/.+\.go$/ do |m|
|
||||||
|
puts "#{Time.now}: #{m[0]}"
|
||||||
|
system 'go build ./cmd/selfupdate-example/'
|
||||||
|
end
|
||||||
|
end
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
the MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 rhysd
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
|
||||||
|
THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
444
README.md
Normal file
444
README.md
Normal file
|
@ -0,0 +1,444 @@
|
||||||
|
Self-Update Mechanism for Go Commands Using GitHub
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
[![GoDoc Badge][]][GoDoc]
|
||||||
|
[![TravisCI Status][]][TravisCI]
|
||||||
|
[![AppVeyor Status][]][AppVeyor]
|
||||||
|
[![Codecov Status][]][Codecov]
|
||||||
|
|
||||||
|
[go-github-selfupdate][] is a Go library to provide a self-update mechanism to command line tools.
|
||||||
|
|
||||||
|
Go does not provide a way to install/update the stable version of tools. By default, Go command line
|
||||||
|
tools are updated:
|
||||||
|
|
||||||
|
1. using `go get -u`, but it is not stable because HEAD of the repository is built
|
||||||
|
2. using system's package manager, but it is harder to release because of depending on the platform
|
||||||
|
3. downloading executables from GitHub release page, but it requires users to download and put it in an executable path manually
|
||||||
|
|
||||||
|
[go-github-selfupdate][] resolves the problem of 3 by detecting the latest release, downloading it and
|
||||||
|
putting it in `$GOPATH/bin` automatically.
|
||||||
|
|
||||||
|
[go-github-selfupdate][] detects the information of the latest release via [GitHub Releases API][] and
|
||||||
|
checks the current version. If a newer version than itself is detected, it downloads the released binary from
|
||||||
|
GitHub and replaces itself.
|
||||||
|
|
||||||
|
- Automatically detect the latest version of released binary on GitHub
|
||||||
|
- Retrieve the proper binary for the OS and arch where the binary is running
|
||||||
|
- 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][]
|
||||||
|
- Support hash, signature validation (thanks to [@tobiaskohlbau](https://github.com/tobiaskohlbau))
|
||||||
|
|
||||||
|
And small wrapper CLIs are provided:
|
||||||
|
|
||||||
|
- [detect-latest-release](./cmd/detect-latest-release): Detect the latest release of given GitHub repository from command line
|
||||||
|
- [go-get-release](./cmd/go-get-release): Like `go get`, but install a release binary from GitHub instead
|
||||||
|
|
||||||
|
[Slide at GoCon 2018 Spring (Japanese)](https://speakerdeck.com/rhysd/go-selfupdate-github-de-turuwozi-ji-atupudetosuru)
|
||||||
|
|
||||||
|
[go-github-selfupdate]: https://github.com/rhysd/go-github-selfupdate
|
||||||
|
[GitHub Releases API]: https://developer.github.com/v3/repos/releases/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Try Out Example
|
||||||
|
|
||||||
|
Example to understand what this library does is prepared as [CLI](./cmd/selfupdate-example/main.go).
|
||||||
|
|
||||||
|
Install it at first.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go get -u github.com/rhysd/go-github-selfupdate/cmd/selfupdate-example
|
||||||
|
```
|
||||||
|
|
||||||
|
And check the version by `-version`. `-help` flag is also available to know all flags.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ selfupdate-example -version
|
||||||
|
```
|
||||||
|
|
||||||
|
It should show `v1.2.3`.
|
||||||
|
|
||||||
|
Then run `-selfupdate`
|
||||||
|
|
||||||
|
```
|
||||||
|
$ selfupdate-example -selfupdate
|
||||||
|
```
|
||||||
|
|
||||||
|
It should replace itself and finally show a message containing release notes.
|
||||||
|
|
||||||
|
Please check the binary version is updated to `v1.2.4` with `-version`. The binary is up-to-date.
|
||||||
|
So running `-selfupdate` again only shows 'Current binary is the latest version'.
|
||||||
|
|
||||||
|
### Real World Examples
|
||||||
|
|
||||||
|
Following tools are using this library.
|
||||||
|
|
||||||
|
- [dot-github](https://github.com/rhysd/dot-github)
|
||||||
|
- [dotfiles](https://github.com/rhysd/dotfiles)
|
||||||
|
- [github-clone-all](https://github.com/rhysd/github-clone-all)
|
||||||
|
- [pythonbrew](https://github.com/utahta/pythonbrew)
|
||||||
|
- [akashic](https://github.com/cowlick/akashic)
|
||||||
|
- [butler](https://github.com/netzkern/butler)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Code Usage
|
||||||
|
|
||||||
|
It provides `selfupdate` package.
|
||||||
|
|
||||||
|
- `selfupdate.UpdateSelf()`: Detect the latest version of itself and run self update.
|
||||||
|
- `selfupdate.UpdateCommand()`: Detect the latest version of given repository and update given command.
|
||||||
|
- `selfupdate.DetectLatest()`: Detect the latest version of given repository.
|
||||||
|
- `selfupdate.DetectVersion()`: Detect the user defined version of given repository.
|
||||||
|
- `selfupdate.UpdateTo()`: Update given command to the binary hosted on given URL.
|
||||||
|
- `selfupdate.Updater`: Context manager of self-update 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.
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"github.com/blang/semver"
|
||||||
|
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
||||||
|
)
|
||||||
|
|
||||||
|
const version = "1.2.3"
|
||||||
|
|
||||||
|
func doSelfUpdate() {
|
||||||
|
v := semver.MustParse(version)
|
||||||
|
latest, err := selfupdate.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Following asks user to update or not.
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"github.com/blang/semver"
|
||||||
|
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const version = "1.2.3"
|
||||||
|
|
||||||
|
func confirmAndSelfUpdate() {
|
||||||
|
latest, found, err := selfupdate.DetectLatest("owner/repo")
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error occurred while detecting version:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
v := semver.MustParse(version)
|
||||||
|
if !found || latest.Version.LTE(v) {
|
||||||
|
log.Println("Current version is the latest")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("Do you want to update to", latest.Version, "? (y/n): ")
|
||||||
|
input, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||||
|
if err != nil || (input != "y\n" && input != "n\n") {
|
||||||
|
log.Println("Invalid input")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if input == "n\n" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Could not locate executable path")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := selfupdate.UpdateTo(latest.AssetURL, exe); err != nil {
|
||||||
|
log.Println("Error occurred while updating binary:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("Successfully updated to version", latest.Version)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Note that `os.Args[0]` is not available since it does not provide a full path to executable. Instead,
|
||||||
|
please use `os.Executable()`.
|
||||||
|
|
||||||
|
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 methods instead (actually all functions above are just a shortcuts of methods of an
|
||||||
|
`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 the `EnterpriseUploadURL`
|
||||||
|
field.
|
||||||
|
|
||||||
|
|
||||||
|
### Naming Rules of Released Binaries
|
||||||
|
|
||||||
|
go-github-selfupdate assumes that released binaries are put for each combination of platforms and archs.
|
||||||
|
Binaries for each platform can be easily built using tools like [gox][]
|
||||||
|
|
||||||
|
You need to put the binaries with the following format.
|
||||||
|
|
||||||
|
```
|
||||||
|
{cmd}_{goos}_{goarch}{.ext}
|
||||||
|
```
|
||||||
|
|
||||||
|
`{cmd}` is a name of command.
|
||||||
|
`{goos}` and `{goarch}` are the platform and the arch type of the binary.
|
||||||
|
`{.ext}` is a file extension. go-github-selfupdate supports `.zip`, `.gzip`, `.tar.gz` and `.tar.xz`.
|
||||||
|
You can also use blank and it means binary is not compressed.
|
||||||
|
|
||||||
|
If you compress binary, uncompressed directory or file must contain the executable named `{cmd}`.
|
||||||
|
|
||||||
|
And you can also use `-` for separator instead of `_` if you like.
|
||||||
|
|
||||||
|
For example, if your command name is `foo-bar`, one of followings is expected to be put in release
|
||||||
|
page on GitHub as binary for platform `linux` and arch `amd64`.
|
||||||
|
|
||||||
|
- `foo-bar_linux_amd64` (executable)
|
||||||
|
- `foo-bar_linux_amd64.zip` (zip file)
|
||||||
|
- `foo-bar_linux_amd64.tar.gz` (tar file)
|
||||||
|
- `foo-bar_linux_amd64.xz` (xzip file)
|
||||||
|
- `foo-bar-linux-amd64.tar.gz` (`-` is also ok for separator)
|
||||||
|
|
||||||
|
If you compress and/or archive your release asset, it must contain an executable named one of followings:
|
||||||
|
|
||||||
|
- `foo-bar` (only command name)
|
||||||
|
- `foo-bar_linux_amd64` (full name)
|
||||||
|
- `foo-bar-linux-amd64` (`-` is also ok for separator)
|
||||||
|
|
||||||
|
To archive the executable directly on Windows, `.exe` can be added before file extension like
|
||||||
|
`foo-bar_windows_amd64.exe.zip`.
|
||||||
|
|
||||||
|
[gox]: https://github.com/mitchellh/gox
|
||||||
|
|
||||||
|
|
||||||
|
### Naming Rules of Versions (=Git Tags)
|
||||||
|
|
||||||
|
go-github-selfupdate searches binaries' versions via Git tag names (not a release title).
|
||||||
|
When your tool's version is `1.2.3`, you should use the version number for tag of the Git
|
||||||
|
repository (i.e. `1.2.3` or `v1.2.3`).
|
||||||
|
|
||||||
|
This library assumes you adopt [semantic versioning][]. It is necessary for comparing versions
|
||||||
|
systematically.
|
||||||
|
|
||||||
|
Prefix before version number `\d+\.\d+\.\d+` is automatically omitted. For example, `ver1.2.3` or
|
||||||
|
`release-1.2.3` are also ok.
|
||||||
|
|
||||||
|
Tags which don't contain a version number are ignored (i.e. `nightly`). And releases marked as `pre-release`
|
||||||
|
are also ignored.
|
||||||
|
|
||||||
|
[semantic versioning]: https://semver.org/
|
||||||
|
|
||||||
|
|
||||||
|
### Structure of Releases
|
||||||
|
|
||||||
|
In summary, structure of releases on GitHub looks like:
|
||||||
|
|
||||||
|
- `v1.2.0`
|
||||||
|
- `foo-bar-linux-amd64.tar.gz`
|
||||||
|
- `foo-bar-linux-386.tar.gz`
|
||||||
|
- `foo-bar-darwin-amd64.tar.gz`
|
||||||
|
- `foo-bar-windows-amd64.zip`
|
||||||
|
- ... (Other binaries for v1.2.0)
|
||||||
|
- `v1.1.3`
|
||||||
|
- `foo-bar-linux-amd64.tar.gz`
|
||||||
|
- `foo-bar-linux-386.tar.gz`
|
||||||
|
- `foo-bar-darwin-amd64.tar.gz`
|
||||||
|
- `foo-bar-windows-amd64.zip`
|
||||||
|
- ... (Other binaries for v1.1.3)
|
||||||
|
- ... (older versions)
|
||||||
|
|
||||||
|
|
||||||
|
### Hash or Signature Validation
|
||||||
|
|
||||||
|
go-github-selfupdate supports hash or signature validatiom of the downloaded files. It comes
|
||||||
|
with support for sha256 hashes or ECDSA signatures. In addition to internal functions the
|
||||||
|
user can implement the `Validator` interface for own validation mechanisms.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Validator represents an interface which enables additional validation of releases.
|
||||||
|
type Validator interface {
|
||||||
|
// Validate validates release bytes against an additional asset bytes.
|
||||||
|
// See SHA2Validator or ECDSAValidator for more information.
|
||||||
|
Validate(release, asset []byte) error
|
||||||
|
// Suffix describes the additional file ending which is used for finding the
|
||||||
|
// additional asset.
|
||||||
|
Suffix() string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SHA256
|
||||||
|
|
||||||
|
To verify the integrity by SHA256 generate a hash sum and save it within a file which has the
|
||||||
|
same naming as original file with the suffix `.sha256`.
|
||||||
|
For e.g. use sha256sum, the file `selfupdate/testdata/foo.zip.sha256` is generated with:
|
||||||
|
```shell
|
||||||
|
sha256sum foo.zip > foo.zip.sha256
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ECDSA
|
||||||
|
To verify the signature by ECDSA generate a signature and save it within a file which has the
|
||||||
|
same naming as original file with the suffix `.sig`.
|
||||||
|
For e.g. use openssl, the file `selfupdate/testdata/foo.zip.sig` is generated with:
|
||||||
|
```shell
|
||||||
|
openssl dgst -sha256 -sign Test.pem -out foo.zip.sig foo.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
go-github-selfupdate makes use of go internal crypto package. Therefore the used private key
|
||||||
|
has to be compatbile with FIPS 186-3.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Running tests
|
||||||
|
|
||||||
|
All library sources are put in `/selfupdate` directory. So you can run tests as following
|
||||||
|
at the top of the repository:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go test -v ./selfupdate
|
||||||
|
```
|
||||||
|
|
||||||
|
Some tests are not run without setting a GitHub API token because they call GitHub API too many times.
|
||||||
|
To run them, please generate an API token and set it to an environment variable.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ export GITHUB_TOKEN="{token generated by you}"
|
||||||
|
$ go test -v ./selfupdate
|
||||||
|
```
|
||||||
|
|
||||||
|
The above command runs almost all tests and it's enough to check the behavior before creating a pull request.
|
||||||
|
Some tests are still not tested because they depend on my personal API access token, though; for repositories
|
||||||
|
on GitHub Enterprise or private repositories on GitHub.
|
||||||
|
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
This library can output logs for debugging. By default, logger is disabled.
|
||||||
|
You can enable the logger by the following and can know the details of the self update.
|
||||||
|
|
||||||
|
```go
|
||||||
|
selfupdate.EnableLog()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
Tests run on CIs (Travis CI, Appveyor) are run with the token I generated. However, because of security
|
||||||
|
reasons, it is not used for the tests for pull requests. In the tests, a GitHub API token is not set and
|
||||||
|
API rate limit is often exceeding. So please ignore the test failures on creating a pull request.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
This library utilizes
|
||||||
|
- [go-github][] to retrieve the information of releases
|
||||||
|
- [go-update][] to replace current binary
|
||||||
|
- [semver][] to compare versions
|
||||||
|
- [xz][] to support XZ compress format
|
||||||
|
|
||||||
|
> Copyright (c) 2013 The go-github AUTHORS. All rights reserved.
|
||||||
|
|
||||||
|
> Copyright 2015 Alan Shreve
|
||||||
|
|
||||||
|
> Copyright (c) 2014 Benedikt Lang <github at benediktlang.de>
|
||||||
|
|
||||||
|
> Copyright (c) 2014-2016 Ulrich Kunitz
|
||||||
|
|
||||||
|
[go-github]: https://github.com/google/go-github
|
||||||
|
[go-update]: https://github.com/inconshreveable/go-update
|
||||||
|
[semver]: https://github.com/blang/semver
|
||||||
|
[xz]: https://github.com/ulikunitz/xz
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## What is different from [tj/go-update][]?
|
||||||
|
|
||||||
|
This library's goal is the same as tj/go-update, but it's different in following points.
|
||||||
|
|
||||||
|
tj/go-update:
|
||||||
|
|
||||||
|
- does not support Windows
|
||||||
|
- only allows `v` for version prefix
|
||||||
|
- does not ignore pre-release
|
||||||
|
- has [only a few tests](https://github.com/tj/go-update/blob/master/update_test.go)
|
||||||
|
- supports Apex store for putting releases
|
||||||
|
|
||||||
|
[tj/go-update]: https://github.com/tj/go-update
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Distributed under the [MIT License](LICENSE)
|
||||||
|
|
||||||
|
[GoDoc Badge]: https://godoc.org/github.com/rhysd/go-github-selfupdate/selfupdate?status.svg
|
||||||
|
[GoDoc]: https://godoc.org/github.com/rhysd/go-github-selfupdate/selfupdate
|
||||||
|
[TravisCI Status]: https://travis-ci.org/rhysd/go-github-selfupdate.svg?branch=master
|
||||||
|
[TravisCI]: https://travis-ci.org/rhysd/go-github-selfupdate
|
||||||
|
[AppVeyor Status]: https://ci.appveyor.com/api/projects/status/1tpyd9q9tw3ime5u/branch/master?svg=true
|
||||||
|
[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
|
20
cmd/detect-latest-release/README.md
Normal file
20
cmd/detect-latest-release/README.md
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
This command line tool is a small wrapper of [`selfupdate.DetectLatest()`](https://godoc.org/github.com/rhysd/go-github-selfupdate/selfupdate#DetectLatest).
|
||||||
|
|
||||||
|
Please install using `go get`.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go get -u github.com/rhysd/go-github-selfupdate/cmd/detect-latest-release
|
||||||
|
```
|
||||||
|
|
||||||
|
To know the usage, please try the command without any argument.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ detect-latest-release
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, following shows the latest version of [github-clone-all](https://github.com/rhysd/github-clone-all).
|
||||||
|
|
||||||
|
```
|
||||||
|
$ detect-latest-release rhysd/github-clone-all
|
||||||
|
```
|
||||||
|
|
66
cmd/detect-latest-release/main.go
Normal file
66
cmd/detect-latest-release/main.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Fprintln(os.Stderr, "Usage: detect-latest-release [flags] {repo}\n\n {repo} must be URL to GitHub repository or in 'owner/name' format.\n\nFlags:\n")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
asset := flag.Bool("asset", false, "Output URL to asset")
|
||||||
|
notes := flag.Bool("release-notes", false, "Output release notes additionally")
|
||||||
|
url := flag.Bool("url", false, "Output URL for release page")
|
||||||
|
|
||||||
|
flag.Usage = usage
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if flag.NArg() != 1 {
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := flag.Arg(0)
|
||||||
|
if strings.HasPrefix(repo, "https://") {
|
||||||
|
repo = repo[len("https://"):]
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(repo, "github.com/") {
|
||||||
|
repo = repo[len("github.com/"):]
|
||||||
|
}
|
||||||
|
|
||||||
|
matched, err := regexp.MatchString("[^/]+/[^/]+", repo)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
latest, found, err := selfupdate.DetectLatest(repo)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
fmt.Println("No release was found")
|
||||||
|
} else {
|
||||||
|
if *asset {
|
||||||
|
fmt.Println(latest.AssetURL)
|
||||||
|
} else if *url {
|
||||||
|
fmt.Println(latest.URL)
|
||||||
|
} else {
|
||||||
|
fmt.Println(latest.Version)
|
||||||
|
if *notes {
|
||||||
|
fmt.Printf("\nRelease Notes:\n%s\n", latest.ReleaseNotes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
cmd/go-get-release/README.md
Normal file
29
cmd/go-get-release/README.md
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
Like `go get`, but it downloads and installs the latest release binary from GitHub instead.
|
||||||
|
|
||||||
|
Please download a binary from [release page](https://github.com/rhysd/go-github-selfupdate/releases/tag/go-get-release)
|
||||||
|
and put it in `$PATH` or build from source with `go get`.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go get -u github.com/rhysd/go-github-selfupdate/cmd/go-get-release
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage is quite similar to `go get`. But `{package}` must be hosted on GitHub. So it needs to start with `github.com/`.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go-get-release {package}
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that this command assumes that specified package is following Git tag naming rules and
|
||||||
|
released binaries naming rules described in [README](../../README.md).
|
||||||
|
|
||||||
|
For example, following command downloads and installs the released binary of [ghr](https://github.com/tcnksm/ghr)
|
||||||
|
to `$GOPATH/bin`.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go-get-release github.com/tcnksm/ghr
|
||||||
|
Command was updated to the latest version 0.5.4: /Users/you/.go/bin/ghr
|
||||||
|
|
||||||
|
$ ghr -version
|
||||||
|
ghr version v0.5.4 (a12ff1c)
|
||||||
|
```
|
||||||
|
|
132
cmd/go-get-release/main.go
Normal file
132
cmd/go-get-release/main.go
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
||||||
|
"go/build"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var version = "1.0.0"
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Fprintln(os.Stderr, `Usage: go-get-release [flags] {package}
|
||||||
|
|
||||||
|
go-get-release is like "go get", but it downloads the latest release from
|
||||||
|
GitHub. {package} must start with "github.com/".
|
||||||
|
|
||||||
|
Flags:`)
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommand(pkg string) string {
|
||||||
|
_, cmd := filepath.Split(pkg)
|
||||||
|
if cmd == "" {
|
||||||
|
// When pkg path is ending with path separator, we need to split it out.
|
||||||
|
// i.e. github.com/rhysd/foo/cmd/bar/
|
||||||
|
_, cmd = filepath.Split(cmd)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSlug(pkg string) (string, bool) {
|
||||||
|
pkg = pkg[len("github.com/"):]
|
||||||
|
first := false
|
||||||
|
for i, r := range pkg {
|
||||||
|
if r == '/' {
|
||||||
|
if !first {
|
||||||
|
first = true
|
||||||
|
} else {
|
||||||
|
return pkg[:i], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if first {
|
||||||
|
// When 'github.com/foo/bar' is specified, reaching here.
|
||||||
|
return pkg, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func installFrom(url, cmd, path string) error {
|
||||||
|
res, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to download release binary from %s: %s", url, err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("Failed to download release binary from %s: Invalid response ", url)
|
||||||
|
}
|
||||||
|
executable, err := selfupdate.UncompressCommand(res.Body, url, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to uncompress downloaded asset from %s: %s", url, err)
|
||||||
|
}
|
||||||
|
bin, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(bin, executable); err != nil {
|
||||||
|
return fmt.Errorf("Failed to write binary to %s: %s", path, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
help := flag.Bool("help", false, "Show help")
|
||||||
|
ver := flag.Bool("version", false, "Show version")
|
||||||
|
|
||||||
|
flag.Usage = usage
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *ver {
|
||||||
|
fmt.Println(version)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *help || flag.NArg() != 1 || !strings.HasPrefix(flag.Arg(0), "github.com/") {
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
slug, ok := parseSlug(flag.Arg(0))
|
||||||
|
if !ok {
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
latest, found, err := selfupdate.DetectLatest(slug)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error while detecting the latest version:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
fmt.Fprintln(os.Stderr, "No release was found in", slug)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := getCommand(flag.Arg(0))
|
||||||
|
cmdPath := filepath.Join(build.Default.GOPATH, "bin", cmd)
|
||||||
|
if _, err := os.Stat(cmdPath); err != nil {
|
||||||
|
// When executable is not existing yet
|
||||||
|
if err := installFrom(latest.AssetURL, cmd, cmdPath); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error while installing the release binary from %s: %s\n", latest.AssetURL, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := selfupdate.UpdateTo(latest.AssetURL, cmdPath); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error while replacing the binary with %s: %s\n", latest.AssetURL, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(`Command was updated to the latest version %s: %s
|
||||||
|
|
||||||
|
Release Notes:
|
||||||
|
%s
|
||||||
|
`, latest.Version, cmdPath, latest.ReleaseNotes)
|
||||||
|
}
|
64
cmd/selfupdate-example/main.go
Normal file
64
cmd/selfupdate-example/main.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"github.com/blang/semver"
|
||||||
|
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const version = "1.2.3"
|
||||||
|
|
||||||
|
func selfUpdate(slug string) error {
|
||||||
|
selfupdate.EnableLog()
|
||||||
|
|
||||||
|
previous := semver.MustParse(version)
|
||||||
|
latest, err := selfupdate.UpdateSelf(previous, slug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if previous.Equals(latest.Version) {
|
||||||
|
fmt.Println("Current binary is the latest version", version)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Update successfully done to version", latest.Version)
|
||||||
|
fmt.Println("Release note:\n", latest.ReleaseNotes)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Fprintln(os.Stderr, "Usage: selfupdate-example [flags]\n")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
help := flag.Bool("help", false, "Show this help")
|
||||||
|
ver := flag.Bool("version", false, "Show version")
|
||||||
|
update := flag.Bool("selfupdate", false, "Try go-github-selfupdate via GitHub")
|
||||||
|
slug := flag.String("slug", "rhysd/go-github-selfupdate", "Repository of this command")
|
||||||
|
|
||||||
|
flag.Usage = usage
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *help {
|
||||||
|
usage()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *ver {
|
||||||
|
fmt.Println(version)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *update {
|
||||||
|
if err := selfUpdate(*slug); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
usage()
|
||||||
|
}
|
18
go.mod
Normal file
18
go.mod
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
module github.com/rhysd/go-github-selfupdate
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/blang/semver v3.5.1+incompatible
|
||||||
|
github.com/google/go-github/v30 v30.1.0
|
||||||
|
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf
|
||||||
|
github.com/kr/pretty v0.1.0 // indirect
|
||||||
|
github.com/onsi/gomega v1.4.2 // indirect
|
||||||
|
github.com/tcnksm/go-gitconfig v0.1.2
|
||||||
|
github.com/ulikunitz/xz v0.5.9
|
||||||
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
|
||||||
|
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288
|
||||||
|
golang.org/x/text v0.3.5 // indirect
|
||||||
|
google.golang.org/appengine v1.3.0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
go 1.13
|
66
go.sum
Normal file
66
go.sum
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||||
|
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
|
||||||
|
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
|
||||||
|
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||||
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
|
||||||
|
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
|
||||||
|
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
||||||
|
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
||||||
|
github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
|
||||||
|
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
||||||
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 h1:JIqe8uIcRBHXDQVvZtHwp80ai3Lw3IJAeJEs55Dc1W0=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||||
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk=
|
||||||
|
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
26
scripts/make-release.sh
Executable file
26
scripts/make-release.sh
Executable file
|
@ -0,0 +1,26 @@
|
||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ ! -d .git ]; then
|
||||||
|
echo 'Run this script from root of repository' 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
executable=selfupdate-example
|
||||||
|
|
||||||
|
rm -rf release
|
||||||
|
gox -verbose ./cmd/$executable
|
||||||
|
mkdir -p release
|
||||||
|
mv selfupdate-example_* release/
|
||||||
|
cd release
|
||||||
|
for bin in *; do
|
||||||
|
if [[ "$bin" == *windows* ]]; then
|
||||||
|
command="${executable}.exe"
|
||||||
|
else
|
||||||
|
command="$executable"
|
||||||
|
fi
|
||||||
|
mv "$bin" "$command"
|
||||||
|
zip "${bin}.zip" "$command"
|
||||||
|
rm "$command"
|
||||||
|
done
|
206
selfupdate/detect.go
Normal file
206
selfupdate/detect.go
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/blang/semver"
|
||||||
|
"github.com/google/go-github/v30/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
var reVersion = regexp.MustCompile(`\d+\.\d+\.\d+`)
|
||||||
|
|
||||||
|
func findAssetFromRelease(rel *github.RepositoryRelease,
|
||||||
|
suffixes []string, targetVersion string, filters []*regexp.Regexp) (*github.ReleaseAsset, semver.Version, bool) {
|
||||||
|
|
||||||
|
if targetVersion != "" && targetVersion != rel.GetTagName() {
|
||||||
|
log.Println("Skip", rel.GetTagName(), "not matching to specified version", targetVersion)
|
||||||
|
return nil, semver.Version{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetVersion == "" && rel.GetDraft() {
|
||||||
|
log.Println("Skip draft version", rel.GetTagName())
|
||||||
|
return nil, semver.Version{}, false
|
||||||
|
}
|
||||||
|
if targetVersion == "" && rel.GetPrerelease() {
|
||||||
|
log.Println("Skip pre-release version", rel.GetTagName())
|
||||||
|
return nil, semver.Version{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
verText := rel.GetTagName()
|
||||||
|
indices := reVersion.FindStringIndex(verText)
|
||||||
|
if indices == nil {
|
||||||
|
log.Println("Skip version not adopting semver", verText)
|
||||||
|
return nil, semver.Version{}, false
|
||||||
|
}
|
||||||
|
if indices[0] > 0 {
|
||||||
|
log.Println("Strip prefix of version", verText[:indices[0]], "from", verText)
|
||||||
|
verText = verText[indices[0]:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// If semver cannot parse the version text, it means that the text is not adopting
|
||||||
|
// the semantic versioning. So it should be skipped.
|
||||||
|
ver, err := semver.Make(verText)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to parse a semantic version", verText)
|
||||||
|
return nil, semver.Version{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, asset := range rel.Assets {
|
||||||
|
name := asset.GetName()
|
||||||
|
if len(filters) > 0 {
|
||||||
|
// if some filters are defined, match them: if any one matches, the asset is selected
|
||||||
|
matched := false
|
||||||
|
for _, filter := range filters {
|
||||||
|
if filter.MatchString(name) {
|
||||||
|
log.Println("Selected filtered asset", name)
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
log.Printf("Skipping asset %q not matching filter %v\n", name, filter)
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range suffixes {
|
||||||
|
if strings.HasSuffix(name, s) { // require version, arch etc
|
||||||
|
// default: assume single artifact
|
||||||
|
return asset, ver, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("No suitable asset was found in release", rel.GetTagName())
|
||||||
|
return nil, semver.Version{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func findValidationAsset(rel *github.RepositoryRelease, validationName string) (*github.ReleaseAsset, bool) {
|
||||||
|
for _, asset := range rel.Assets {
|
||||||
|
if asset.GetName() == validationName {
|
||||||
|
return asset, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func findReleaseAndAsset(rels []*github.RepositoryRelease,
|
||||||
|
targetVersion string,
|
||||||
|
filters []*regexp.Regexp) (*github.RepositoryRelease, *github.ReleaseAsset, semver.Version, bool) {
|
||||||
|
// Generate candidates
|
||||||
|
suffixes := make([]string, 0, 2*7*2)
|
||||||
|
for _, sep := range []rune{'_', '-'} {
|
||||||
|
for _, ext := range []string{".zip", ".tar.gz", ".tgz", ".gzip", ".gz", ".tar.xz", ".xz", ""} {
|
||||||
|
suffix := fmt.Sprintf("%s%c%s%s", runtime.GOOS, sep, runtime.GOARCH, ext)
|
||||||
|
suffixes = append(suffixes, suffix)
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
suffix = fmt.Sprintf("%s%c%s.exe%s", runtime.GOOS, sep, runtime.GOARCH, ext)
|
||||||
|
suffixes = append(suffixes, suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ver semver.Version
|
||||||
|
var asset *github.ReleaseAsset
|
||||||
|
var release *github.RepositoryRelease
|
||||||
|
|
||||||
|
// Find the latest version from the list of releases.
|
||||||
|
// Returned list from GitHub API is in the order of the date when created.
|
||||||
|
// ref: https://github.com/rhysd/go-github-selfupdate/issues/11
|
||||||
|
for _, rel := range rels {
|
||||||
|
if a, v, ok := findAssetFromRelease(rel, suffixes, targetVersion, filters); ok {
|
||||||
|
// Note: any version with suffix is less than any version without suffix.
|
||||||
|
// e.g. 0.0.1 > 0.0.1-beta
|
||||||
|
if release == nil || v.GTE(ver) {
|
||||||
|
ver = v
|
||||||
|
asset = a
|
||||||
|
release = rel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if release == nil {
|
||||||
|
log.Println("Could not find any release for", runtime.GOOS, "and", runtime.GOARCH)
|
||||||
|
return nil, nil, semver.Version{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return release, asset, ver, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectLatest tries to get the latest version of the repository on GitHub. 'slug' means 'owner/name' formatted string.
|
||||||
|
// It fetches releases information from GitHub API and find out the latest release with matching the tag names and asset names.
|
||||||
|
// Drafts and pre-releases are ignored. Assets would be suffixed by the OS name and the arch name such as 'foo_linux_amd64'
|
||||||
|
// where 'foo' is a command name. '-' can also be used as a separator. File can be compressed with zip, gzip, zxip, tar&zip or tar&zxip.
|
||||||
|
// So the asset can have a file extension for the corresponding compression format such as '.zip'.
|
||||||
|
// On Windows, '.exe' also can be contained such as 'foo_windows_amd64.exe.zip'.
|
||||||
|
func (up *Updater) DetectLatest(slug string) (release *Release, found bool, err error) {
|
||||||
|
return up.DetectVersion(slug, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectVersion tries to get the given version of the repository on Github. `slug` means `owner/name` formatted string.
|
||||||
|
// And version indicates the required version.
|
||||||
|
func (up *Updater) DetectVersion(slug string, version string) (release *Release, found bool, err error) {
|
||||||
|
repo := strings.Split(slug, "/")
|
||||||
|
if len(repo) != 2 || repo[0] == "" || repo[1] == "" {
|
||||||
|
return nil, false, fmt.Errorf("Invalid slug format. It should be 'owner/name': %s", slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// 404 means repository not found or release not found. It's not an error here.
|
||||||
|
err = nil
|
||||||
|
log.Println("API returned 404. Repository or release not found")
|
||||||
|
}
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rel, asset, ver, found := findReleaseAndAsset(rels, version, up.filters)
|
||||||
|
if !found {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
url := asset.GetBrowserDownloadURL()
|
||||||
|
log.Println("Successfully fetched the latest release. tag:", rel.GetTagName(), ", name:", rel.GetName(), ", URL:", rel.GetURL(), ", Asset:", url)
|
||||||
|
|
||||||
|
publishedAt := rel.GetPublishedAt().Time
|
||||||
|
release = &Release{
|
||||||
|
ver,
|
||||||
|
url,
|
||||||
|
asset.GetSize(),
|
||||||
|
asset.GetID(),
|
||||||
|
-1,
|
||||||
|
rel.GetHTMLURL(),
|
||||||
|
rel.GetBody(),
|
||||||
|
rel.GetName(),
|
||||||
|
&publishedAt,
|
||||||
|
repo[0],
|
||||||
|
repo[1],
|
||||||
|
}
|
||||||
|
|
||||||
|
if up.validator != nil {
|
||||||
|
validationName := asset.GetName() + up.validator.Suffix()
|
||||||
|
validationAsset, ok := findValidationAsset(rel, validationName)
|
||||||
|
if !ok {
|
||||||
|
return nil, false, fmt.Errorf("Failed finding validation file %q", validationName)
|
||||||
|
}
|
||||||
|
release.ValidationAssetID = validationAsset.GetID()
|
||||||
|
}
|
||||||
|
|
||||||
|
return release, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectLatest detects the latest release of the slug (owner/repo).
|
||||||
|
// This function is a shortcut version of updater.DetectLatest() method.
|
||||||
|
func DetectLatest(slug string) (*Release, bool, error) {
|
||||||
|
return DefaultUpdater().DetectLatest(slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectVersion detects the given release of the slug (owner/repo) from its version.
|
||||||
|
func DetectVersion(slug string, version string) (*Release, bool, error) {
|
||||||
|
return DefaultUpdater().DetectVersion(slug, version)
|
||||||
|
}
|
457
selfupdate/detect_test.go
Normal file
457
selfupdate/detect_test.go
Normal file
|
@ -0,0 +1,457 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/blang/semver"
|
||||||
|
"github.com/google/go-github/v30/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDetectReleaseWithVersionPrefix(t *testing.T) {
|
||||||
|
r, ok, err := DetectLatest("rhysd/github-clone-all")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Fetch failed:", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Failed to detect latest")
|
||||||
|
}
|
||||||
|
if r == nil {
|
||||||
|
t.Fatal("Release detected but nil returned for it")
|
||||||
|
}
|
||||||
|
if r.Version.LE(semver.MustParse("2.0.0")) {
|
||||||
|
t.Error("Incorrect version:", r.Version)
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(r.AssetURL, ".zip") && !strings.HasSuffix(r.AssetURL, ".tar.gz") {
|
||||||
|
t.Error("Incorrect URL for asset:", r.AssetURL)
|
||||||
|
}
|
||||||
|
if r.URL == "" {
|
||||||
|
t.Error("Document URL should not be empty")
|
||||||
|
}
|
||||||
|
if r.ReleaseNotes == "" {
|
||||||
|
t.Error("Description should not be empty for this repo")
|
||||||
|
}
|
||||||
|
if r.Name == "" {
|
||||||
|
t.Error("Release name is unexpectedly empty")
|
||||||
|
}
|
||||||
|
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 TestDetectVersionExisting(t *testing.T) {
|
||||||
|
testVersion := "v2.2.0"
|
||||||
|
r, ok, err := DetectVersion("rhysd/github-clone-all", testVersion)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Fetch failed:", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Failed to detect %s", testVersion)
|
||||||
|
}
|
||||||
|
if r == nil {
|
||||||
|
t.Fatal("Release detected but nil returned for it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectVersionNotExisting(t *testing.T) {
|
||||||
|
r, ok, err := DetectVersion("rhysd/github-clone-all", "foobar")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Fetch failed:", err)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
t.Fatal("Failed to correctly detect foobar")
|
||||||
|
}
|
||||||
|
if r != nil {
|
||||||
|
t.Fatal("Release not detected but got a returned value for it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectReleasesForVariousArchives(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
slug string
|
||||||
|
prefix string
|
||||||
|
}{
|
||||||
|
{"rhysd-test/test-release-zip", "v"},
|
||||||
|
{"rhysd-test/test-release-tar", "v"},
|
||||||
|
{"rhysd-test/test-release-gzip", "v"},
|
||||||
|
{"rhysd-test/test-release-xz", "release-v"},
|
||||||
|
{"rhysd-test/test-release-tar-xz", "release-"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.slug, func(t *testing.T) {
|
||||||
|
r, ok, err := DetectLatest(tc.slug)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Fetch failed:", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Fatal(tc.slug, "not found")
|
||||||
|
}
|
||||||
|
if r == nil {
|
||||||
|
t.Fatal("Release not detected")
|
||||||
|
}
|
||||||
|
if !r.Version.Equals(semver.MustParse("1.2.3")) {
|
||||||
|
t.Error("")
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("https://github.com/%s/releases/tag/%s1.2.3", tc.slug, tc.prefix)
|
||||||
|
if r.URL != url {
|
||||||
|
t.Error("URL is not correct. Want", url, "but got", r.URL)
|
||||||
|
}
|
||||||
|
if r.ReleaseNotes == "" {
|
||||||
|
t.Error("Release note is unexpectedly empty")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(r.AssetURL, fmt.Sprintf("https://github.com/%s/releases/download/%s1.2.3/", tc.slug, tc.prefix)) {
|
||||||
|
t.Error("Unexpected asset URL:", r.AssetURL)
|
||||||
|
}
|
||||||
|
if r.Name == "" {
|
||||||
|
t.Error("Release name is unexpectedly empty")
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectReleaseButNoAsset(t *testing.T) {
|
||||||
|
_, ok, err := DetectLatest("rhysd/clever-f.vim")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Fetch failed:", err)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
t.Fatal("When no asset found, result should be marked as 'not found'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectNoRelease(t *testing.T) {
|
||||||
|
_, ok, err := DetectLatest("rhysd/clever-f.vim")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Fetch failed:", err)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
t.Fatal("When no release found, result should be marked as 'not found'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidSlug(t *testing.T) {
|
||||||
|
up := DefaultUpdater()
|
||||||
|
|
||||||
|
for _, slug := range []string{
|
||||||
|
"foo",
|
||||||
|
"/",
|
||||||
|
"foo/",
|
||||||
|
"/bar",
|
||||||
|
"foo/bar/piyo",
|
||||||
|
} {
|
||||||
|
_, _, err := up.DetectLatest(slug)
|
||||||
|
if err == nil {
|
||||||
|
t.Error(slug, "should be invalid slug")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "Invalid slug format") {
|
||||||
|
t.Error("Unexpected error for", slug, ":", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonExistingRepo(t *testing.T) {
|
||||||
|
v, ok, err := 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) {
|
||||||
|
_, ok, err := 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindReleaseAndAsset(t *testing.T) {
|
||||||
|
EnableLog()
|
||||||
|
type findReleaseAndAssetFixture struct {
|
||||||
|
name string
|
||||||
|
rels *github.RepositoryRelease
|
||||||
|
targetVersion string
|
||||||
|
filters []*regexp.Regexp
|
||||||
|
expectedAsset string
|
||||||
|
expectedVersion string
|
||||||
|
expectedFound bool
|
||||||
|
}
|
||||||
|
|
||||||
|
rel1 := "rel1"
|
||||||
|
v1 := "1.0.0"
|
||||||
|
rel11 := "rel11"
|
||||||
|
v11 := "1.1.0"
|
||||||
|
asset1 := "asset1.gz"
|
||||||
|
asset2 := "asset2.gz"
|
||||||
|
wrongAsset1 := "asset1.yaml"
|
||||||
|
asset11 := "asset11.gz"
|
||||||
|
url1 := "https://asset1"
|
||||||
|
url2 := "https://asset2"
|
||||||
|
url11 := "https://asset11"
|
||||||
|
for _, fixture := range []findReleaseAndAssetFixture{
|
||||||
|
{
|
||||||
|
name: "empty fixture",
|
||||||
|
rels: nil,
|
||||||
|
targetVersion: "",
|
||||||
|
filters: nil,
|
||||||
|
expectedFound: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find asset, no filters",
|
||||||
|
rels: &github.RepositoryRelease{
|
||||||
|
Name: &rel1,
|
||||||
|
TagName: &v1,
|
||||||
|
Assets: []*github.ReleaseAsset{
|
||||||
|
{
|
||||||
|
Name: &asset1,
|
||||||
|
URL: &url1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetVersion: "1.0.0",
|
||||||
|
expectedAsset: asset1,
|
||||||
|
expectedVersion: "1.0.0",
|
||||||
|
expectedFound: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "don't find asset with wrong extension, no filters",
|
||||||
|
rels: &github.RepositoryRelease{
|
||||||
|
Name: &rel11,
|
||||||
|
TagName: &v11,
|
||||||
|
Assets: []*github.ReleaseAsset{
|
||||||
|
{
|
||||||
|
Name: &wrongAsset1,
|
||||||
|
URL: &url11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetVersion: "1.1.0",
|
||||||
|
expectedFound: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find asset with different name, no filters",
|
||||||
|
rels: &github.RepositoryRelease{
|
||||||
|
Name: &rel11,
|
||||||
|
TagName: &v11,
|
||||||
|
Assets: []*github.ReleaseAsset{
|
||||||
|
{
|
||||||
|
Name: &asset1,
|
||||||
|
URL: &url11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetVersion: "1.1.0",
|
||||||
|
expectedAsset: asset1,
|
||||||
|
expectedVersion: "1.1.0",
|
||||||
|
expectedFound: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find asset, no filters (2)",
|
||||||
|
rels: &github.RepositoryRelease{
|
||||||
|
Name: &rel11,
|
||||||
|
TagName: &v11,
|
||||||
|
Assets: []*github.ReleaseAsset{
|
||||||
|
{
|
||||||
|
Name: &asset11,
|
||||||
|
URL: &url11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetVersion: "1.1.0",
|
||||||
|
expectedAsset: asset11,
|
||||||
|
expectedVersion: "1.1.0",
|
||||||
|
filters: nil,
|
||||||
|
expectedFound: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find asset, match filter",
|
||||||
|
rels: &github.RepositoryRelease{
|
||||||
|
Name: &rel11,
|
||||||
|
TagName: &v11,
|
||||||
|
Assets: []*github.ReleaseAsset{
|
||||||
|
{
|
||||||
|
Name: &asset11,
|
||||||
|
URL: &url11,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: &asset1,
|
||||||
|
URL: &url1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetVersion: "1.1.0",
|
||||||
|
filters: []*regexp.Regexp{regexp.MustCompile("11")},
|
||||||
|
expectedAsset: asset11,
|
||||||
|
expectedVersion: "1.1.0",
|
||||||
|
expectedFound: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find asset, match another filter",
|
||||||
|
rels: &github.RepositoryRelease{
|
||||||
|
Name: &rel11,
|
||||||
|
TagName: &v11,
|
||||||
|
Assets: []*github.ReleaseAsset{
|
||||||
|
{
|
||||||
|
Name: &asset11,
|
||||||
|
URL: &url11,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: &asset1,
|
||||||
|
URL: &url1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetVersion: "1.1.0",
|
||||||
|
filters: []*regexp.Regexp{regexp.MustCompile("([^1])1{1}([^1])")},
|
||||||
|
expectedAsset: asset1,
|
||||||
|
expectedVersion: "1.1.0",
|
||||||
|
expectedFound: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find asset, match any filter",
|
||||||
|
rels: &github.RepositoryRelease{
|
||||||
|
Name: &rel11,
|
||||||
|
TagName: &v11,
|
||||||
|
Assets: []*github.ReleaseAsset{
|
||||||
|
{
|
||||||
|
Name: &asset11,
|
||||||
|
URL: &url11,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: &asset2,
|
||||||
|
URL: &url2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetVersion: "1.1.0",
|
||||||
|
filters: []*regexp.Regexp{
|
||||||
|
regexp.MustCompile("([^1])1{1}([^1])"),
|
||||||
|
regexp.MustCompile("([^1])2{1}([^1])"),
|
||||||
|
},
|
||||||
|
expectedAsset: asset2,
|
||||||
|
expectedVersion: "1.1.0",
|
||||||
|
expectedFound: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find asset, match no filter",
|
||||||
|
rels: &github.RepositoryRelease{
|
||||||
|
Name: &rel11,
|
||||||
|
TagName: &v11,
|
||||||
|
Assets: []*github.ReleaseAsset{
|
||||||
|
{
|
||||||
|
Name: &asset11,
|
||||||
|
URL: &url11,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: &asset2,
|
||||||
|
URL: &url2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetVersion: "1.1.0",
|
||||||
|
filters: []*regexp.Regexp{
|
||||||
|
regexp.MustCompile("another"),
|
||||||
|
regexp.MustCompile("binary"),
|
||||||
|
},
|
||||||
|
expectedFound: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
asset, ver, found := findAssetFromRelease(fixture.rels, []string{".gz"}, fixture.targetVersion, fixture.filters)
|
||||||
|
if fixture.expectedFound {
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected to find an asset for this fixture: %q", fixture.name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if asset.Name == nil {
|
||||||
|
t.Errorf("invalid asset struct returned from fixture: %q, got: %v", fixture.name, asset)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if *asset.Name != fixture.expectedAsset {
|
||||||
|
t.Errorf("expected asset %q in fixture: %q, got: %s", fixture.expectedAsset, fixture.name, *asset.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Logf("asset %v, %v", asset, ver)
|
||||||
|
} else if found {
|
||||||
|
t.Errorf("expected not to find an asset for this fixture: %q, but got: %v", fixture.name, asset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
38
selfupdate/doc.go
Normal file
38
selfupdate/doc.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
Package selfupdate provides self-update mechanism to Go command line tools.
|
||||||
|
|
||||||
|
Go does not provide the way to install/update the stable version of tools. By default, Go command line tools are updated
|
||||||
|
|
||||||
|
- using `go get -u` (updating to HEAD)
|
||||||
|
- using system's package manager (depending on the platform)
|
||||||
|
- downloading executables from GitHub release page manually
|
||||||
|
|
||||||
|
By using this library, you will get 4th choice:
|
||||||
|
|
||||||
|
- from your command line tool directly (and automatically)
|
||||||
|
|
||||||
|
go-github-selfupdate detects the information of the latest release via GitHub Releases API and check the current version.
|
||||||
|
If newer version than itself is detected, it downloads released binary from GitHub and replaces itself.
|
||||||
|
|
||||||
|
- Automatically detects the latest version of released binary on GitHub
|
||||||
|
- Retrieve the proper binary for the OS and arch where the binary is running
|
||||||
|
- Update the binary with rollback support on failure
|
||||||
|
- Tested on Linux, macOS and Windows
|
||||||
|
- Many archive and compression formats are supported (zip, gzip, xzip, tar)
|
||||||
|
|
||||||
|
There are some naming rules. Please read following links.
|
||||||
|
|
||||||
|
Naming Rules of Released Binaries:
|
||||||
|
https://github.com/rhysd/go-github-selfupdate#naming-rules-of-released-binaries
|
||||||
|
|
||||||
|
Naming Rules of Git Tags:
|
||||||
|
https://github.com/rhysd/go-github-selfupdate#naming-rules-of-git-tags
|
||||||
|
|
||||||
|
This package is hosted on GitHub:
|
||||||
|
https://github.com/rhysd/go-github-selfupdate
|
||||||
|
|
||||||
|
Small CLI tools as wrapper of this library are available also:
|
||||||
|
https://github.com/rhysd/go-github-selfupdate/cmd/detect-latest-release
|
||||||
|
https://github.com/rhysd/go-github-selfupdate/cmd/go-get-release
|
||||||
|
*/
|
||||||
|
package selfupdate
|
22
selfupdate/e2e_test.go
Normal file
22
selfupdate/e2e_test.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunSelfUpdateExample(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping test in short mode.")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Skip("TODO")
|
||||||
|
|
||||||
|
if err := exec.Command("go", "build", "../cmd/selfupdate-example").Run(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove("selfupdate-example")
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
}
|
30
selfupdate/log.go
Normal file
30
selfupdate/log.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
stdlog "log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log = stdlog.New(ioutil.Discard, "", 0)
|
||||||
|
var logEnabled = false
|
||||||
|
|
||||||
|
// EnableLog enables to output logging messages in library
|
||||||
|
func EnableLog() {
|
||||||
|
if logEnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logEnabled = true
|
||||||
|
log.SetOutput(os.Stderr)
|
||||||
|
log.SetFlags(stdlog.Ltime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableLog disables to output logging messages in library
|
||||||
|
func DisableLog() {
|
||||||
|
if !logEnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logEnabled = false
|
||||||
|
log.SetOutput(ioutil.Discard)
|
||||||
|
log.SetFlags(0)
|
||||||
|
}
|
30
selfupdate/log_test.go
Normal file
30
selfupdate/log_test.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnableDisableLog(t *testing.T) {
|
||||||
|
defer DisableLog()
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
33
selfupdate/release.go
Normal file
33
selfupdate/release.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/blang/semver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Release represents a release asset for current OS and arch.
|
||||||
|
type Release struct {
|
||||||
|
// Version is the version of the release
|
||||||
|
Version semver.Version
|
||||||
|
// AssetURL is a URL to the uploaded file for the release
|
||||||
|
AssetURL string
|
||||||
|
// AssetSize represents the size of asset in bytes
|
||||||
|
AssetByteSize int
|
||||||
|
// AssetID is the ID of the asset on GitHub
|
||||||
|
AssetID int64
|
||||||
|
// ValidationAssetID is the ID of additional validaton asset on GitHub
|
||||||
|
ValidationAssetID int64
|
||||||
|
// URL is a URL to release page for browsing
|
||||||
|
URL string
|
||||||
|
// ReleaseNotes is a release notes of the release
|
||||||
|
ReleaseNotes string
|
||||||
|
// Name represents a name of the release
|
||||||
|
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
|
||||||
|
}
|
9
selfupdate/testdata/Test.crt
vendored
Normal file
9
selfupdate/testdata/Test.crt
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBLzCB1qADAgECAgglJIDaK1tbjjAKBggqhkjOPQQDAjAPMQ0wCwYDVQQDEwRU
|
||||||
|
ZXN0MCAXDTE4MTEwNjE1MTcwMFoYDzIxMTgxMTA2MTUxNzAwWjAPMQ0wCwYDVQQD
|
||||||
|
EwRUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs8fjo/Mi5A3c2v2YxV6A
|
||||||
|
QPJnr70qYMEpsmqn0BTcI8RhZUgB46tWqeDYdO15yQKbZjfI/dr0fvS21jyW0GSX
|
||||||
|
rKMaMBgwCwYDVR0PBAQDAgXgMAkGA1UdEwQCMAAwCgYIKoZIzj0EAwIDSAAwRQIh
|
||||||
|
AI1pUr0nrw3m++sR8HEBoejM5Qh1QJA7gF9y1jY6rc/aAiALFPJXckSLAQuq5IvQ
|
||||||
|
7cugOPws7/OoUo1124LKPugISg==
|
||||||
|
-----END CERTIFICATE-----
|
14
selfupdate/testdata/Test.pem
vendored
Normal file
14
selfupdate/testdata/Test.pem
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEIJvTkRedVrQDNjCb9/RfVjzRwz8S059Y1J6w2N8gy8jVoAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAEs8fjo/Mi5A3c2v2YxV6AQPJnr70qYMEpsmqn0BTcI8RhZUgB46tW
|
||||||
|
qeDYdO15yQKbZjfI/dr0fvS21jyW0GSXrA==
|
||||||
|
-----END EC PRIVATE KEY-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBLzCB1qADAgECAgglJIDaK1tbjjAKBggqhkjOPQQDAjAPMQ0wCwYDVQQDEwRU
|
||||||
|
ZXN0MCAXDTE4MTEwNjE1MTcwMFoYDzIxMTgxMTA2MTUxNzAwWjAPMQ0wCwYDVQQD
|
||||||
|
EwRUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs8fjo/Mi5A3c2v2YxV6A
|
||||||
|
QPJnr70qYMEpsmqn0BTcI8RhZUgB46tWqeDYdO15yQKbZjfI/dr0fvS21jyW0GSX
|
||||||
|
rKMaMBgwCwYDVR0PBAQDAgXgMAkGA1UdEwQCMAAwCgYIKoZIzj0EAwIDSAAwRQIh
|
||||||
|
AI1pUr0nrw3m++sR8HEBoejM5Qh1QJA7gF9y1jY6rc/aAiALFPJXckSLAQuq5IvQ
|
||||||
|
7cugOPws7/OoUo1124LKPugISg==
|
||||||
|
-----END CERTIFICATE-----
|
BIN
selfupdate/testdata/bar-not-found.gzip
vendored
Normal file
BIN
selfupdate/testdata/bar-not-found.gzip
vendored
Normal file
Binary file not shown.
BIN
selfupdate/testdata/bar-not-found.tar.gz
vendored
Normal file
BIN
selfupdate/testdata/bar-not-found.tar.gz
vendored
Normal file
Binary file not shown.
BIN
selfupdate/testdata/bar-not-found.tar.xz
vendored
Normal file
BIN
selfupdate/testdata/bar-not-found.tar.xz
vendored
Normal file
Binary file not shown.
BIN
selfupdate/testdata/bar-not-found.zip
vendored
Normal file
BIN
selfupdate/testdata/bar-not-found.zip
vendored
Normal file
Binary file not shown.
BIN
selfupdate/testdata/empty.tar.gz
vendored
Normal file
BIN
selfupdate/testdata/empty.tar.gz
vendored
Normal file
Binary file not shown.
BIN
selfupdate/testdata/empty.zip
vendored
Normal file
BIN
selfupdate/testdata/empty.zip
vendored
Normal file
Binary file not shown.
1
selfupdate/testdata/fake-executable
vendored
Normal file
1
selfupdate/testdata/fake-executable
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
this file is used for passing check of file existence in update tests.
|
1
selfupdate/testdata/fake-executable.exe
vendored
Normal file
1
selfupdate/testdata/fake-executable.exe
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
this file is used for passing check of file existence in update tests.
|
BIN
selfupdate/testdata/foo.tar.gz
vendored
Normal file
BIN
selfupdate/testdata/foo.tar.gz
vendored
Normal file
Binary file not shown.
BIN
selfupdate/testdata/foo.tar.xz
vendored
Normal file
BIN
selfupdate/testdata/foo.tar.xz
vendored
Normal file
Binary file not shown.
BIN
selfupdate/testdata/foo.tgz
vendored
Normal file
BIN
selfupdate/testdata/foo.tgz
vendored
Normal file
Binary file not shown.
BIN
selfupdate/testdata/foo.zip
vendored
Normal file
BIN
selfupdate/testdata/foo.zip
vendored
Normal file
Binary file not shown.
1
selfupdate/testdata/foo.zip.sha256
vendored
Normal file
1
selfupdate/testdata/foo.zip.sha256
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
e412095724426c984940efde02ea000251a12b37506c977341e0a07600dbfcb6 foo.zip
|
BIN
selfupdate/testdata/foo.zip.sig
vendored
Normal file
BIN
selfupdate/testdata/foo.zip.sig
vendored
Normal file
Binary file not shown.
5
selfupdate/testdata/github-release-test/main.go
vendored
Normal file
5
selfupdate/testdata/github-release-test/main.go
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
println("not released yet!")
|
||||||
|
}
|
0
selfupdate/testdata/invalid-gzip.tar.gz
vendored
Normal file
0
selfupdate/testdata/invalid-gzip.tar.gz
vendored
Normal file
BIN
selfupdate/testdata/invalid-tar.tar.gz
vendored
Normal file
BIN
selfupdate/testdata/invalid-tar.tar.gz
vendored
Normal file
Binary file not shown.
BIN
selfupdate/testdata/invalid-tar.tar.xz
vendored
Normal file
BIN
selfupdate/testdata/invalid-tar.tar.xz
vendored
Normal file
Binary file not shown.
1
selfupdate/testdata/invalid-xz.tar.xz
vendored
Normal file
1
selfupdate/testdata/invalid-xz.tar.xz
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
hello
|
0
selfupdate/testdata/invalid.gz
vendored
Normal file
0
selfupdate/testdata/invalid.gz
vendored
Normal file
1
selfupdate/testdata/invalid.xz
vendored
Normal file
1
selfupdate/testdata/invalid.xz
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
hello
|
0
selfupdate/testdata/invalid.zip
vendored
Normal file
0
selfupdate/testdata/invalid.zip
vendored
Normal file
BIN
selfupdate/testdata/single-file.gz
vendored
Normal file
BIN
selfupdate/testdata/single-file.gz
vendored
Normal file
Binary file not shown.
BIN
selfupdate/testdata/single-file.gzip
vendored
Normal file
BIN
selfupdate/testdata/single-file.gzip
vendored
Normal file
Binary file not shown.
BIN
selfupdate/testdata/single-file.xz
vendored
Normal file
BIN
selfupdate/testdata/single-file.xz
vendored
Normal file
Binary file not shown.
BIN
selfupdate/testdata/single-file.zip
vendored
Normal file
BIN
selfupdate/testdata/single-file.zip
vendored
Normal file
Binary file not shown.
136
selfupdate/uncompress.go
Normal file
136
selfupdate/uncompress.go
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"github.com/ulikunitz/xz"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func matchExecutableName(cmd, target string) bool {
|
||||||
|
if cmd == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
o, a := runtime.GOOS, runtime.GOARCH
|
||||||
|
|
||||||
|
// When the contained executable name is full name (e.g. foo_darwin_amd64),
|
||||||
|
// it is also regarded as a target executable file. (#19)
|
||||||
|
for _, d := range []rune{'_', '-'} {
|
||||||
|
c := fmt.Sprintf("%s%c%s%c%s", cmd, d, o, d, a)
|
||||||
|
if o == "windows" {
|
||||||
|
c += ".exe"
|
||||||
|
}
|
||||||
|
if c == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func unarchiveTar(src io.Reader, url, cmd string) (io.Reader, error) {
|
||||||
|
t := tar.NewReader(src)
|
||||||
|
for {
|
||||||
|
h, err := t.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to unarchive .tar file: %s", err)
|
||||||
|
}
|
||||||
|
_, name := filepath.Split(h.Name)
|
||||||
|
if matchExecutableName(cmd, name) {
|
||||||
|
log.Println("Executable file", h.Name, "was found in tar archive")
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("File '%s' for the command is not found in %s", cmd, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UncompressCommand uncompresses the given source. Archive and compression format is
|
||||||
|
// automatically detected from 'url' parameter, which represents the URL of asset.
|
||||||
|
// This returns a reader for the uncompressed command given by 'cmd'. '.zip',
|
||||||
|
// '.tar.gz', '.tar.xz', '.tgz', '.gz' and '.xz' are supported.
|
||||||
|
func UncompressCommand(src io.Reader, url, cmd string) (io.Reader, error) {
|
||||||
|
if strings.HasSuffix(url, ".zip") {
|
||||||
|
log.Println("Uncompressing zip file", url)
|
||||||
|
|
||||||
|
// Zip format requires its file size for uncompressing.
|
||||||
|
// So we need to read the HTTP response into a buffer at first.
|
||||||
|
buf, err := ioutil.ReadAll(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to create buffer for zip file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := bytes.NewReader(buf)
|
||||||
|
z, err := zip.NewReader(r, r.Size())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to uncompress zip file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range z.File {
|
||||||
|
_, name := filepath.Split(file.Name)
|
||||||
|
if !file.FileInfo().IsDir() && matchExecutableName(cmd, name) {
|
||||||
|
log.Println("Executable file", file.Name, "was found in zip archive")
|
||||||
|
return file.Open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("File '%s' for the command is not found in %s", cmd, url)
|
||||||
|
} else if strings.HasSuffix(url, ".tar.gz") || strings.HasSuffix(url, ".tgz") {
|
||||||
|
log.Println("Uncompressing tar.gz file", url)
|
||||||
|
|
||||||
|
gz, err := gzip.NewReader(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to uncompress .tar.gz file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return unarchiveTar(gz, url, cmd)
|
||||||
|
} else if strings.HasSuffix(url, ".gzip") || strings.HasSuffix(url, ".gz") {
|
||||||
|
log.Println("Uncompressing gzip file", url)
|
||||||
|
|
||||||
|
r, err := gzip.NewReader(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to uncompress gzip file downloaded from %s: %s", url, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := r.Header.Name
|
||||||
|
if !matchExecutableName(cmd, name) {
|
||||||
|
return nil, fmt.Errorf("File name '%s' does not match to command '%s' found in %s", name, cmd, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Executable file", name, "was found in gzip file")
|
||||||
|
return r, nil
|
||||||
|
} else if strings.HasSuffix(url, ".tar.xz") {
|
||||||
|
log.Println("Uncompressing tar.xz file", url)
|
||||||
|
|
||||||
|
xzip, err := xz.NewReader(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to uncompress .tar.xz file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return unarchiveTar(xzip, url, cmd)
|
||||||
|
} else if strings.HasSuffix(url, ".xz") {
|
||||||
|
log.Println("Uncompressing xzip file", url)
|
||||||
|
|
||||||
|
xzip, err := xz.NewReader(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to uncompress xzip file downloaded from %s: %s", url, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Uncompressed file from xzip is assumed to be an executable", cmd)
|
||||||
|
return xzip, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Uncompression is not needed", url)
|
||||||
|
return src, nil
|
||||||
|
}
|
133
selfupdate/uncompress_test.go
Normal file
133
selfupdate/uncompress_test.go
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCompressionNotRequired(t *testing.T) {
|
||||||
|
buf := []byte{'a', 'b', 'c'}
|
||||||
|
want := bytes.NewReader(buf)
|
||||||
|
r, err := UncompressCommand(want, "https://github.com/foo/bar/releases/download/v1.2.3/foo", "foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
have, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for i, b := range have {
|
||||||
|
if buf[i] != b {
|
||||||
|
t.Error(i, "th elem is not the same as wanted. want", buf[i], "but got", b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getArchiveFileExt(file string) string {
|
||||||
|
if strings.HasSuffix(file, ".tar.gz") {
|
||||||
|
return ".tar.gz"
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(file, ".tar.xz") {
|
||||||
|
return ".tar.xz"
|
||||||
|
}
|
||||||
|
return filepath.Ext(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUncompress(t *testing.T) {
|
||||||
|
for _, n := range []string{
|
||||||
|
"testdata/foo.zip",
|
||||||
|
"testdata/single-file.zip",
|
||||||
|
"testdata/single-file.gz",
|
||||||
|
"testdata/single-file.gzip",
|
||||||
|
"testdata/foo.tar.gz",
|
||||||
|
"testdata/foo.tgz",
|
||||||
|
"testdata/foo.tar.xz",
|
||||||
|
"testdata/single-file.xz",
|
||||||
|
} {
|
||||||
|
t.Run(n, func(t *testing.T) {
|
||||||
|
f, err := os.Open(n)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := getArchiveFileExt(n)
|
||||||
|
url := "https://github.com/foo/bar/releases/download/v1.2.3/bar" + ext
|
||||||
|
r, err := UncompressCommand(f, url, "bar")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s := string(bytes)
|
||||||
|
if s != "this is test\n" {
|
||||||
|
t.Fatal("Uncompressing zip failed into unexpected content", s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUncompressInvalidArchive(t *testing.T) {
|
||||||
|
for _, a := range []struct {
|
||||||
|
name string
|
||||||
|
msg string
|
||||||
|
}{
|
||||||
|
{"testdata/invalid.zip", "not a valid zip file"},
|
||||||
|
{"testdata/invalid.gz", "Failed to uncompress gzip file"},
|
||||||
|
{"testdata/invalid-tar.tar.gz", "Failed to unarchive .tar file"},
|
||||||
|
{"testdata/invalid-gzip.tar.gz", "Failed to uncompress .tar.gz file"},
|
||||||
|
{"testdata/invalid.xz", "Failed to uncompress xzip file"},
|
||||||
|
{"testdata/invalid-tar.tar.xz", "Failed to unarchive .tar file"},
|
||||||
|
{"testdata/invalid-xz.tar.xz", "Failed to uncompress .tar.xz file"},
|
||||||
|
} {
|
||||||
|
f, err := os.Open(a.name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := getArchiveFileExt(a.name)
|
||||||
|
url := "https://github.com/foo/bar/releases/download/v1.2.3/bar" + ext
|
||||||
|
_, err = UncompressCommand(f, url, "bar")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Error should be raised")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), a.msg) {
|
||||||
|
t.Fatal("Unexpected error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetNotFound(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
msg string
|
||||||
|
}{
|
||||||
|
{"testdata/empty.zip", "command is not found"},
|
||||||
|
{"testdata/bar-not-found.zip", "command is not found"},
|
||||||
|
{"testdata/bar-not-found.gzip", "does not match to command"},
|
||||||
|
{"testdata/empty.tar.gz", "command is not found"},
|
||||||
|
{"testdata/bar-not-found.tar.gz", "command is not found"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
f, err := os.Open(tc.name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ext := getArchiveFileExt(tc.name)
|
||||||
|
url := "https://github.com/foo/bar/releases/download/v1.2.3/bar" + ext
|
||||||
|
_, err = UncompressCommand(f, url, "bar")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Error should be raised for")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tc.msg) {
|
||||||
|
t.Fatal("Unexpected error:", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
181
selfupdate/update.go
Normal file
181
selfupdate/update.go
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/blang/semver"
|
||||||
|
"github.com/inconshreveable/go-update"
|
||||||
|
)
|
||||||
|
|
||||||
|
func uncompressAndUpdate(src io.Reader, assetURL, cmdPath string) error {
|
||||||
|
_, cmd := filepath.Split(cmdPath)
|
||||||
|
asset, err := UncompressCommand(src, assetURL, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Will update", cmdPath, "to the latest downloaded from", assetURL)
|
||||||
|
return update.Apply(asset, update.Options{
|
||||||
|
TargetPath: cmdPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
var client http.Client
|
||||||
|
src, redirectURL, err := up.api.Repositories.DownloadReleaseAsset(up.apiCtx, rel.RepoOwner, rel.RepoName, rel.AssetID, &client)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(src)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed reading asset body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if up.validator == nil {
|
||||||
|
return uncompressAndUpdate(bytes.NewReader(data), rel.AssetURL, cmdPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
validationSrc, validationRedirectURL, err := up.api.Repositories.DownloadReleaseAsset(up.apiCtx, rel.RepoOwner, rel.RepoName, rel.ValidationAssetID, &client)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to call GitHub Releases API for getting an validation asset(ID: %d) for repository '%s/%s': %s", rel.ValidationAssetID, rel.RepoOwner, rel.RepoName, err)
|
||||||
|
}
|
||||||
|
if validationRedirectURL != "" {
|
||||||
|
log.Println("Redirect URL was returned while trying to download a release validation asset from GitHub API. Falling back to downloading from asset URL directly:", redirectURL)
|
||||||
|
validationSrc, err = up.downloadDirectlyFromURL(validationRedirectURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer validationSrc.Close()
|
||||||
|
|
||||||
|
validationData, err := ioutil.ReadAll(validationSrc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed reading validation asset body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := up.validator.Validate(data, validationData); err != nil {
|
||||||
|
return fmt.Errorf("Failed validating asset content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uncompressAndUpdate(bytes.NewReader(data), 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 (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"
|
||||||
|
}
|
||||||
|
|
||||||
|
stat, err := os.Lstat(cmdPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to stat '%s'. File may not exist: %s", cmdPath, err)
|
||||||
|
}
|
||||||
|
if stat.Mode()&os.ModeSymlink != 0 {
|
||||||
|
p, err := filepath.EvalSymlinks(cmdPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to resolve symlink '%s' for executable: %s", cmdPath, err)
|
||||||
|
}
|
||||||
|
cmdPath = p
|
||||||
|
}
|
||||||
|
|
||||||
|
rel, ok, err := up.DetectLatest(slug)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
log.Println("No release detected. Current version is considered up-to-date")
|
||||||
|
return &Release{Version: current}, nil
|
||||||
|
}
|
||||||
|
if current.Equals(rel.Version) {
|
||||||
|
log.Println("Current version", current, "is the latest. Update is not needed")
|
||||||
|
return rel, nil
|
||||||
|
}
|
||||||
|
log.Println("Will update", cmdPath, "to the latest version", rel.Version)
|
||||||
|
if err := up.UpdateTo(rel, cmdPath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSelf updates the running executable itself to the latest version.
|
||||||
|
// 'slug' represents 'owner/name' repository on GitHub and 'current' means the current version.
|
||||||
|
func (up *Updater) UpdateSelf(current semver.Version, slug string) (*Release, error) {
|
||||||
|
cmdPath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return up.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
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
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)
|
||||||
|
}
|
353
selfupdate/update_test.go
Normal file
353
selfupdate/update_test.go
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/blang/semver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTestBinary() {
|
||||||
|
if err := exec.Command("go", "build", "./testdata/github-release-test/").Run(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func teardownTestBinary() {
|
||||||
|
bin := "github-release-test"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
bin = "github-release-test.exe"
|
||||||
|
}
|
||||||
|
if err := os.Remove(bin); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateCommand(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skip tests in short mode.")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, slug := range []string{
|
||||||
|
"rhysd-test/test-release-zip",
|
||||||
|
"rhysd-test/test-release-tar",
|
||||||
|
"rhysd-test/test-release-gzip",
|
||||||
|
"rhysd-test/test-release-tar-xz",
|
||||||
|
"rhysd-test/test-release-xz",
|
||||||
|
"rhysd-test/test-release-contain-version",
|
||||||
|
} {
|
||||||
|
t.Run(slug, func(t *testing.T) {
|
||||||
|
setupTestBinary()
|
||||||
|
defer teardownTestBinary()
|
||||||
|
latest := semver.MustParse("1.2.3")
|
||||||
|
prev := semver.MustParse("1.2.2")
|
||||||
|
rel, err := UpdateCommand("github-release-test", prev, slug)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateViaSymlink(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skip tests in short mode.")
|
||||||
|
}
|
||||||
|
if runtime.GOOS == "windows" && os.Getenv("APPVEYOR") == "" {
|
||||||
|
t.Skip("skipping because creating symlink on windows requires the root privilege")
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTestBinary()
|
||||||
|
defer teardownTestBinary()
|
||||||
|
exePath := "github-release-test"
|
||||||
|
symPath := "github-release-test-sym"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
exePath = "github-release-test.exe"
|
||||||
|
symPath = "github-release-test-sym.exe"
|
||||||
|
}
|
||||||
|
if err := os.Symlink(exePath, symPath); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(symPath)
|
||||||
|
|
||||||
|
latest := semver.MustParse("1.2.3")
|
||||||
|
prev := semver.MustParse("1.2.2")
|
||||||
|
rel, err := UpdateCommand(symPath, prev, "rhysd-test/test-release-zip")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if rel.Version.NE(latest) {
|
||||||
|
t.Error("Version is not latest", rel.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test not symbolic link, but actual physical executable
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := os.Lstat(symPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if s.Mode()&os.ModeSymlink == 0 {
|
||||||
|
t.Fatalf("%s is not a symlink.", symPath)
|
||||||
|
}
|
||||||
|
p, err := filepath.EvalSymlinks(symPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if p != exePath {
|
||||||
|
t.Fatal("Created symlink no loger points the executable:", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateBrokenSymlinks(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" && os.Getenv("APPVEYOR") == "" {
|
||||||
|
t.Skip("skipping because creating symlink on windows requires the root privilege")
|
||||||
|
}
|
||||||
|
|
||||||
|
// unknown-xxx -> unknown-yyy -> {not existing}
|
||||||
|
xxx := "unknown-xxx"
|
||||||
|
yyy := "unknown-yyy"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
xxx = "unknown-xxx.exe"
|
||||||
|
yyy = "unknown-yyy.exe"
|
||||||
|
}
|
||||||
|
if err := os.Symlink("not-existing", yyy); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(yyy)
|
||||||
|
if err := os.Symlink(yyy, xxx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(xxx)
|
||||||
|
|
||||||
|
v := semver.MustParse("1.2.2")
|
||||||
|
for _, p := range []string{yyy, xxx} {
|
||||||
|
_, err := UpdateCommand(p, v, "owner/repo")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Error should occur for unlinked symlink", p)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "Failed to resolve symlink") {
|
||||||
|
t.Fatal("Unexpected error for broken symlink", p, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotExistingCommandPath(t *testing.T) {
|
||||||
|
_, err := UpdateCommand("not-existing-command-path", semver.MustParse("1.2.2"), "owner/repo")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Not existing command path should cause an error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "File may not exist") {
|
||||||
|
t.Fatal("Unexpected error for not existing command path", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoReleaseFoundForUpdate(t *testing.T) {
|
||||||
|
v := semver.MustParse("1.0.0")
|
||||||
|
fake := filepath.FromSlash("./testdata/fake-executable")
|
||||||
|
rel, err := UpdateCommand(fake, v, "rhysd/misc")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("No release should not make an error:", err)
|
||||||
|
}
|
||||||
|
if rel.Version.NE(v) {
|
||||||
|
t.Error("No release should return the current version as the latest:", rel.Version)
|
||||||
|
}
|
||||||
|
if rel.URL != "" {
|
||||||
|
t.Error("Browse URL should be empty when no release found:", rel.URL)
|
||||||
|
}
|
||||||
|
if rel.AssetURL != "" {
|
||||||
|
t.Error("Asset URL should be empty when no release found:", rel.AssetURL)
|
||||||
|
}
|
||||||
|
if rel.ReleaseNotes != "" {
|
||||||
|
t.Error("Release notes should be empty when no release found:", rel.ReleaseNotes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCurrentIsTheLatest(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skip tests in short mode.")
|
||||||
|
}
|
||||||
|
setupTestBinary()
|
||||||
|
defer teardownTestBinary()
|
||||||
|
|
||||||
|
v := semver.MustParse("1.2.3")
|
||||||
|
rel, err := UpdateCommand("github-release-test", v, "rhysd-test/test-release-zip")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if rel.Version.NE(v) {
|
||||||
|
t.Error("v1.2.3 should be the latest:", rel.Version)
|
||||||
|
}
|
||||||
|
if rel.URL == "" {
|
||||||
|
t.Error("Browse URL should not be empty when release found:", rel.URL)
|
||||||
|
}
|
||||||
|
if rel.AssetURL == "" {
|
||||||
|
t.Error("Asset URL should not be empty when release found:", rel.AssetURL)
|
||||||
|
}
|
||||||
|
if rel.ReleaseNotes == "" {
|
||||||
|
t.Error("Release notes should not be empty when release found:", rel.ReleaseNotes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBrokenBinaryUpdate(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skip tests in short mode.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fake := filepath.FromSlash("./testdata/fake-executable")
|
||||||
|
_, err := UpdateCommand(fake, semver.MustParse("1.2.2"), "rhysd-test/test-incorrect-release")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Error should occur for broken package")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "Failed to uncompress .tar.gz file") {
|
||||||
|
t.Fatal("Unexpected error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidSlugForUpdate(t *testing.T) {
|
||||||
|
fake := filepath.FromSlash("./testdata/fake-executable")
|
||||||
|
_, err := UpdateCommand(fake, semver.MustParse("1.0.0"), "rhysd/")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Unknown repo should cause an error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "Invalid slug format") {
|
||||||
|
t.Fatal("Unexpected error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidAssetURL(t *testing.T) {
|
||||||
|
err := 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")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "Failed to download a release file") {
|
||||||
|
t.Fatal("Unexpected error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBrokenAsset(t *testing.T) {
|
||||||
|
asset := "https://github.com/rhysd-test/test-incorrect-release/releases/download/invalid/broken-zip.zip"
|
||||||
|
err := UpdateTo(asset, "foo")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Error should occur for URL not found")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "Failed to uncompress zip file") {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateFromGitHubPrivateRepo(t *testing.T) {
|
||||||
|
token := os.Getenv("GITHUB_PRIVATE_TOKEN")
|
||||||
|
if token == "" {
|
||||||
|
t.Skip("because GITHUB_PRIVATE_TOKEN is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTestBinary()
|
||||||
|
defer teardownTestBinary()
|
||||||
|
|
||||||
|
up, err := NewUpdater(Config{APIToken: token})
|
||||||
|
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, "rhysd/private-release-test")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
99
selfupdate/updater.go
Normal file
99
selfupdate/updater.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/google/go-github/v30/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
|
||||||
|
validator Validator
|
||||||
|
filters []*regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// Validator represents types which enable additional validation of downloaded release.
|
||||||
|
Validator Validator
|
||||||
|
// Filters are regexp used to filter on specific assets for releases with multiple assets.
|
||||||
|
// An asset is selected if it matches any of those, in addition to the regular tag, os, arch, extensions.
|
||||||
|
// Please make sure that your filter(s) uniquely match an asset.
|
||||||
|
Filters []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)
|
||||||
|
|
||||||
|
filtersRe := make([]*regexp.Regexp, 0, len(config.Filters))
|
||||||
|
for _, filter := range config.Filters {
|
||||||
|
re, err := regexp.Compile(filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Could not compile regular expression %q for filtering releases: %v", filter, err)
|
||||||
|
}
|
||||||
|
filtersRe = append(filtersRe, re)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.EnterpriseBaseURL == "" {
|
||||||
|
client := github.NewClient(hc)
|
||||||
|
return &Updater{api: client, apiCtx: ctx, validator: config.Validator, filters: filtersRe}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u := config.EnterpriseUploadURL
|
||||||
|
if u == "" {
|
||||||
|
u = config.EnterpriseBaseURL
|
||||||
|
}
|
||||||
|
client, err := github.NewEnterpriseClient(config.EnterpriseBaseURL, u, hc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Updater{api: client, apiCtx: ctx, validator: config.Validator, filters: filtersRe}, 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{api: github.NewClient(client), apiCtx: ctx}
|
||||||
|
}
|
106
selfupdate/updater_test.go
Normal file
106
selfupdate/updater_test.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"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 TestGitHubTokenIsNotSet(t *testing.T) {
|
||||||
|
token := os.Getenv("GITHUB_TOKEN")
|
||||||
|
if token != "" {
|
||||||
|
defer os.Setenv("GITHUB_TOKEN", token)
|
||||||
|
}
|
||||||
|
os.Setenv("GITHUB_TOKEN", "")
|
||||||
|
_ = DefaultUpdater()
|
||||||
|
if _, err := NewUpdater(Config{}); err != nil {
|
||||||
|
t.Error("Failed to initialize updater with empty 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 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompileRegexForFiltering(t *testing.T) {
|
||||||
|
filters := []string{
|
||||||
|
"^hello$",
|
||||||
|
"^(\\d\\.)+\\d$",
|
||||||
|
}
|
||||||
|
up, err := NewUpdater(Config{
|
||||||
|
Filters: filters,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(up.filters) != 2 {
|
||||||
|
t.Fatalf("Wanted 2 regexes but got %d", len(up.filters))
|
||||||
|
}
|
||||||
|
for i, r := range up.filters {
|
||||||
|
want := filters[i]
|
||||||
|
got := r.String()
|
||||||
|
if want != got {
|
||||||
|
t.Errorf("Compiled regex is %q but specified was %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterRegexIsBroken(t *testing.T) {
|
||||||
|
_, err := NewUpdater(Config{
|
||||||
|
Filters: []string{"(foo"},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Error unexpectedly did not occur")
|
||||||
|
}
|
||||||
|
msg := err.Error()
|
||||||
|
if !strings.Contains(msg, "Could not compile regular expression \"(foo\" for filtering releases") {
|
||||||
|
t.Fatalf("Error message is unexpected: %q", msg)
|
||||||
|
}
|
||||||
|
}
|
73
selfupdate/validate.go
Normal file
73
selfupdate/validate.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/asn1"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validator represents an interface which enables additional validation of releases.
|
||||||
|
type Validator interface {
|
||||||
|
// Validate validates release bytes against an additional asset bytes.
|
||||||
|
// See SHA2Validator or ECDSAValidator for more information.
|
||||||
|
Validate(release, asset []byte) error
|
||||||
|
// Suffix describes the additional file ending which is used for finding the
|
||||||
|
// additional asset.
|
||||||
|
Suffix() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SHA2Validator specifies a SHA256 validator for additional file validation
|
||||||
|
// before updating.
|
||||||
|
type SHA2Validator struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the SHA256 sum of the release against the contents of an
|
||||||
|
// additional asset file.
|
||||||
|
func (v *SHA2Validator) Validate(release, asset []byte) error {
|
||||||
|
calculatedHash := fmt.Sprintf("%x", sha256.Sum256(release))
|
||||||
|
hash := fmt.Sprintf("%s", asset[:sha256.BlockSize])
|
||||||
|
if calculatedHash != hash {
|
||||||
|
return fmt.Errorf("sha2: validation failed: hash mismatch: expected=%q, got=%q", calculatedHash, hash)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suffix returns the suffix for SHA2 validation.
|
||||||
|
func (v *SHA2Validator) Suffix() string {
|
||||||
|
return ".sha256"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECDSAValidator specifies a ECDSA validator for additional file validation
|
||||||
|
// before updating.
|
||||||
|
type ECDSAValidator struct {
|
||||||
|
PublicKey *ecdsa.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the ECDSA signature the release against the signature
|
||||||
|
// contained in an additional asset file.
|
||||||
|
// additional asset file.
|
||||||
|
func (v *ECDSAValidator) Validate(input, signature []byte) error {
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(input)
|
||||||
|
|
||||||
|
var rs struct {
|
||||||
|
R *big.Int
|
||||||
|
S *big.Int
|
||||||
|
}
|
||||||
|
if _, err := asn1.Unmarshal(signature, &rs); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal ecdsa signature: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ecdsa.Verify(v.PublicKey, h.Sum([]byte{}), rs.R, rs.S) {
|
||||||
|
return fmt.Errorf("ecdsa: signature verification failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suffix returns the suffix for ECDSA validation.
|
||||||
|
func (v *ECDSAValidator) Suffix() string {
|
||||||
|
return ".sig"
|
||||||
|
}
|
136
selfupdate/validate_test.go
Normal file
136
selfupdate/validate_test.go
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSHA2Validator(t *testing.T) {
|
||||||
|
validator := &SHA2Validator{}
|
||||||
|
data, err := ioutil.ReadFile("testdata/foo.zip")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
hashData, err := ioutil.ReadFile("testdata/foo.zip.sha256")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := validator.Validate(data, hashData); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSHA2ValidatorFail(t *testing.T) {
|
||||||
|
validator := &SHA2Validator{}
|
||||||
|
data, err := ioutil.ReadFile("testdata/foo.zip")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
hashData, err := ioutil.ReadFile("testdata/foo.zip.sha256")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
hashData[0] = '0'
|
||||||
|
if err := validator.Validate(data, hashData); err == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECDSAValidator(t *testing.T) {
|
||||||
|
pemData, err := ioutil.ReadFile("testdata/Test.crt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode(pemData)
|
||||||
|
if block == nil || block.Type != "CERTIFICATE" {
|
||||||
|
t.Fatalf("failed to decode PEM block")
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey, ok := cert.PublicKey.(*ecdsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("PublicKey is not ECDSA")
|
||||||
|
}
|
||||||
|
|
||||||
|
validator := &ECDSAValidator{
|
||||||
|
PublicKey: pubKey,
|
||||||
|
}
|
||||||
|
data, err := ioutil.ReadFile("testdata/foo.zip")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
signatureData, err := ioutil.ReadFile("testdata/foo.zip.sig")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := validator.Validate(data, signatureData); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECDSAValidatorFail(t *testing.T) {
|
||||||
|
pemData, err := ioutil.ReadFile("testdata/Test.crt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode(pemData)
|
||||||
|
if block == nil || block.Type != "CERTIFICATE" {
|
||||||
|
t.Fatalf("failed to decode PEM block")
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey, ok := cert.PublicKey.(*ecdsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("PublicKey is not ECDSA")
|
||||||
|
}
|
||||||
|
|
||||||
|
validator := &ECDSAValidator{
|
||||||
|
PublicKey: pubKey,
|
||||||
|
}
|
||||||
|
data, err := ioutil.ReadFile("testdata/foo.tar.xz")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
signatureData, err := ioutil.ReadFile("testdata/foo.zip.sig")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := validator.Validate(data, signatureData); err == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatorSuffix(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
v Validator
|
||||||
|
suffix string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
v: &SHA2Validator{},
|
||||||
|
suffix: ".sha256",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: &ECDSAValidator{},
|
||||||
|
suffix: ".sig",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
want := test.suffix
|
||||||
|
got := test.v.Suffix()
|
||||||
|
if want != got {
|
||||||
|
t.Errorf("Wanted %q but got %q", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue