Compare commits

...

137 Commits

Author SHA1 Message Date
Renovate Bot 14b987cb42 Update module github.com/blang/semver to v3.8.0 2022-06-10 18:06:24 +02:00
Renovate Bot fed5b400de Update module github.com/ulikunitz/xz to v0.5.10 2022-06-10 18:06:13 +02:00
Oskar 5aea9fd5f3
Update go.yml 2021-10-27 23:01:16 +02:00
MedzikUser e33f9e522e update 2021-10-27 22:59:25 +02:00
MedzikUser 542dc5db61 Revert "update"
This reverts commit c9e5fe26ca.
2021-10-27 22:56:40 +02:00
MedzikUser 4b201b07f5 update 2021-10-27 22:54:30 +02:00
MedzikUser c9e5fe26ca update 2021-10-27 22:54:03 +02:00
MedzikUser e8ecb189e5 update 2021-10-27 22:48:35 +02:00
MedzikUser a75b2492cb update 2021-10-27 22:41:38 +02:00
MedzikUser 8a2cf5e596 update 2021-10-27 22:35:41 +02:00
Oskar ec571e8c14
Create go.yml 2021-10-27 22:26:02 +02:00
MedzikUser 9c76d66885 update 2021-10-27 22:24:36 +02:00
MedzikUser 9abefd0460 update 2021-10-27 22:23:51 +02:00
Oskar 8ab4b94ea1
Update renovate.json 2021-10-27 22:18:50 +02:00
Oskar 13766db3e1
Create renovate.json 2021-08-18 20:46:00 +02:00
Medzik f29763a683 Update 2021-07-24 19:39:58 +00:00
rhysd 04a545f8ea update changelog for v1.2.3 2021-01-13 21:25:45 +09:00
Linda_pp 1aa7d81a46
Merge pull request #38 from bhamail/update_x_text
update x/text. Fix CVE-2020-14040
2021-01-13 16:43:16 +09:00
Dan Rollo f315b89e29 update x/text. Fix CVE-2020-14040 2021-01-12 23:03:40 -05:00
Linda_pp 4161c14e0c
Merge pull request #37 from bhamail/update_x_crypto
update x/crypto. Fix CVE-2019-11840
2021-01-13 12:55:27 +09:00
Dan Rollo ed69ef6c6b Merge branch 'master' into update_x_crypto
# Conflicts:
#	go.mod
2021-01-12 22:48:47 -05:00
Linda_pp 466402f90d
Merge pull request #36 from bhamail/update_xz
update to latest release of xz lib. Fix CVE-2020-16845
2021-01-13 10:41:36 +09:00
Dan Rollo a894e020c3 update x/crypto. Fix CVE-2019-11840 2021-01-12 19:03:43 -05:00
Dan Rollo 40792dc985 update to latest release of xz lib. Fix CVE-2020-16845 2021-01-12 18:31:49 -05:00
Linda_pp 5766a1046b
Merge pull request #29 from michaelbirdflyt/master
update to go-github v30.1.0
2020-04-10 15:26:13 +09:00
Mike Bird 17cd45ef2c update to go-github v30.1.0 2020-04-09 17:28:13 -07:00
rhysd 6cf6ce8c7a update changelog for v1.2.1 2019-12-19 18:08:27 +09:00
rhysd 905eb9f670 fix .tgz file is not handled on UncompressCommand 2019-12-19 16:28:00 +09:00
rhysd 3b082d183d update CHANGELOG for v1.2.0 2019-12-19 13:08:04 +09:00
rhysd b5118a9ebf test validator file suffix 2019-12-19 11:40:54 +09:00
rhysd a4d48e7891 add tests for initializing filter regexes 2019-12-19 11:28:39 +09:00
rhysd 351a78b1eb improve error message when regex for filter is broken 2019-12-19 11:28:08 +09:00
Linda_pp 53849d13bc
Merge pull request #25 from fredbi/master
Added option to filter multiple assets in release
2019-12-19 10:49:43 +09:00
Frederic BIDON 12e3317156
nitpick: added cr on logging
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
2019-12-18 18:34:02 +01:00
Frederic BIDON 7fc1c7ebec
tidy up unit test
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
2019-12-18 09:49:52 +01:00
Frederic BIDON 63b0fb3fc1
* reverted choice of Option functor and added filters to Config
* moved regexp-base filtering loop upwards (fail early)
* added unit test for asset lookup with or without filters

Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
2019-12-18 09:32:48 +01:00
Frederic BIDON 0148193fae
Added option to filter multiple assets in release
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
2019-12-16 20:12:11 +01:00
Linda_pp 80762b859d
Merge pull request #22 from flowonyx/patch-2
Corrected some typos in README
2019-10-30 12:39:08 +09:00
Joel Williams 677875a59b
Corrected some typos
Made some corrections to grammar and deleted a repeated section. I hope this is helpful for you.
2019-10-30 00:25:24 +02:00
Linda_pp c328ee2035
Merge pull request #21 from pieterclaerhout/master
Update detect.go to add support for tgz files
2019-10-20 00:30:02 +09:00
Pieter Claerhout fdad9d2127
Update detect.go
https://github.com/rhysd/go-github-selfupdate/issues/20
2019-10-19 12:46:25 +02:00
rhysd 04c8f18cca describe executable file name convention contained in assets 2019-07-25 18:37:40 +09:00
rhysd 051f309b75 allow {cmd}_{os}_{arch} name for executable in asset (#19) 2019-07-25 18:31:38 +09:00
rhysd 87b01fbf06 remove go.sum temporarily
It seems that how to calculate hashes is depending on Go
toolchain toolchain version. It means that this library may not be
installed due to 'broken hash' with different Go toolchain from mine. I
believe it is temporal since Go module feature is actively developed.

I decided to remove `go.sum` temporarily. It should be restored in the
future.
2019-03-01 20:11:33 +09:00
Linda_pp 5592b885d8
Merge pull request #18 from bertuss/patch-1
Fix small typo in Readme
2019-01-14 01:11:49 +09:00
Bertus Steenberg 687eda561c
s/dpeneding/depending/ 2019-01-13 17:18:19 +02:00
rhysd ff8cf0cdf6 versioning go-get-release 2018-11-10 23:16:45 +09:00
rhysd 8823218ebf add changelog 2018-11-10 13:08:05 +09:00
rhysd b179163923 tweak README [skip ci] 2018-11-10 12:52:51 +09:00
rhysd 472a014a45 update dependencies with `go get -u` and `go mod tidy` 2018-11-10 12:48:44 +09:00
Linda_pp a77177617a
Merge pull request #16 from tobiaskohlbau/addHashSignature
Add hash and signature validation
2018-11-09 20:00:26 +09:00
Tobias Kohlbau 116dfa144d Add hash and signature validation
Go-self-update lacks support for checking integrity of downloaded files.
For more advanced situation it's necessary to validate the hash or
verify against public signatures. This patch adds support for
SHA2 hash and ECDSA PublicKey signature validation.
SHA2 uses file with suffix `.sha256`, whereas ECDSA uses
`.sig` file endings. See `selfupdate/validate_test.go` for examples.

Signed-off-by: Tobias Kohlbau <t.kohlbau@myopenfactory.com>
2018-11-09 11:21:21 +01:00
rhysd db4f96105a use os.Executable() and .LTE() for safety in example 2018-11-06 19:39:58 +09:00
Linda_pp 6329a43531
Merge pull request #15 from tobiaskohlbau/patch-1
Fix bad go get within readme
2018-10-18 19:53:24 +09:00
Tobias Kohlbau 63988a901a
Fix bad go get within readme
Go get works with package names not with urls.
2018-10-18 11:04:52 +02:00
rhysd 4bfc1aa030 Merge branch 'gomod' 2018-09-23 20:52:01 +09:00
rhysd 23ccef091e use Go modules on CI 2018-09-23 20:49:15 +09:00
rhysd cc92a5255f introduce Go module 2018-09-23 20:37:06 +09:00
rhysd 41c1bbb080 Merge branch 'run-tests-shortly-on-PR' 2018-05-20 23:23:21 +09:00
rhysd 796b216a75 I don't want to waste time with Appveyor 2018-05-20 23:22:28 +09:00
rhysd 006a0d3c1f run tests with -short flag on pull requests
because they would fail since $GITHUB_TOKEN is not set
2018-05-20 22:15:41 +09:00
Linda_pp c7abcd03e1
Merge pull request #13 from databus23/patch-2
Do not require GITHUB_TOKEN for enterprise setups
2018-05-20 21:50:52 +09:00
rhysd 967270aa69 remove TestGitHubEnterpriseClientWithoutToken which is no longer valid due to 41acec2 2018-05-20 21:49:57 +09:00
Linda_pp 953b97fcd1
Merge pull request #12 from databus23/patch-1
fix UpdateSelf invocation on custom Updater object
2018-04-30 21:34:28 +09:00
Fabian Ruff 41acec25c4 Do not require GITHUB_TOKEN for enterprise setups
Not sure why this was added but using no token works for our GitHub enterprise installation.
2018-04-27 17:12:36 +02:00
Fabian Ruff d1140d3bf8
fix UpdateSelf invocation on custom Updater object 2018-04-27 16:07:51 +02:00
rhysd ef42c4e258 fix typo 2018-04-19 17:58:07 +09:00
rhysd 6f1eb0320d [skip ci] add link to a deck at GoCon 2018 Spring 2018-04-17 21:51:40 +09:00
rhysd 4f7f22f940 revise introduction and add license notice of xz 2018-03-28 11:00:13 +09:00
rhysd 8e171e57c0 ignore coverage report file 2018-03-28 10:52:32 +09:00
rhysd c93b65e513 github.Releases.DownloadReleaseAsset() was fixed 2018-03-27 15:42:23 +09:00
Linda_pp d782c99504
fix typo 2018-03-22 08:55:31 +09:00
rhysd 8cf7b9f527 avoid go-github issue on CI 2018-03-22 05:21:24 +09:00
rhysd 74c96838ff add notice about https://github.com/google/go-github/issues/870 2018-03-22 05:09:28 +09:00
rhysd d475f34e08 fix misspells 2018-03-21 19:18:46 +09:00
Linda_pp c0c9635893
Merge pull request #10 from jbfarez/feature/custom-version-support
Add support to pass custom version to fetch from Github
2018-03-21 19:05:43 +09:00
Jean-Baptiste Farez a3573534f9
Add support to pass custom version to fetch from Github 2018-03-13 09:16:15 +01:00
rhysd d5c53b8d05 do not assume tag text is always semantic versioning
regex \d+\.\d+\.\d+ is tested for repository tags. But it's not
sufficient. For example, '0.1.2.3.4' is not adopting a semantic
versioning, but can pass the regex.
2018-02-12 00:05:51 +09:00
rhysd 9b705a72eb refactoring: positional fields for struct and unnamed return value
- positional fields are better because it raises an error when missing
  field is detected
- unnamed return value is better because it introduces simpler control
  sequence. Previously I used it because rel.Version is added after
  and the parse may cause an error. But now it is parsed more earlier
  and not required in DetectLatest().
2018-02-11 23:50:21 +09:00
rhysd 281445119d describe how to run tests and about CI 2018-02-11 23:36:50 +09:00
rhysd 02a5d46e86 do not assume a descending order (fix #11)
for testing, v0.0.9 is added to test-release-tar repository.

    https://github.com/rhysd-test/test-release-tar/releases
2018-02-11 23:22:08 +09:00
rhysd 7bcefbd709 refactoring: parse a version when an asset is detected 2018-02-11 22:57:15 +09:00
rhysd 56328bb821 more real-world examples 2018-02-06 15:48:03 +09:00
rhysd a384a5b5c8 test creating an updater when token is not set 2018-02-06 14:01:43 +09:00
rhysd 2bc49bb7b5 add more log on detection and fix a log message 2018-02-06 13:47:59 +09:00
rhysd 8fd62fcf16 remove httpClient field of Updater instance which is not used 2018-02-06 11:57:30 +09:00
rhysd 7b77ed8d85 tweak document of DetectLatest() 2018-02-06 11:57:30 +09:00
Jean-Baptiste Farez 05a876f32d Fix go-github int64 requirements (#9) 2018-02-02 18:07:47 +09:00
rhysd 083b14b51a add a test for private repositories 2018-01-20 00:34:50 +09:00
rhysd 8a8c4e5c9a Merge branch 'updater' (fix #2, fix #6) 2018-01-19 19:58:20 +09:00
rhysd a8d7262d70 add tests for GitHub Enterprise more
These tests require GHE environment.
Please set following environment variables:

- $GITHUB_ENTERPRISE_TOKEN
- $GITHUB_ENTERPRISE_BASE_URL
- $GITHUB_ENTERPRISE_REPO
2018-01-19 19:49:45 +09:00
rhysd 89582ba9d1 add more test cases to check invalid GitHub Enterprise base URL 2018-01-19 18:43:29 +09:00
rhysd 1265b4826d add tests to create an updater instance for GitHub Enterprise 2018-01-19 18:13:38 +09:00
rhysd f637165ab7 add description about `Updater` struct usage for GitHub Enterprise 2018-01-19 18:04:30 +09:00
rhysd d0de60fba1 enable to have enterprise base URL and upload URL 2018-01-19 17:47:21 +09:00
rhysd 996452a5ed use default HTTP client to download blob from redirect URL 2018-01-19 17:00:30 +09:00
rhysd d98ca18787 fix handling redirection of GitHub download asset API 2018-01-19 15:03:47 +09:00
rhysd 6b4eeadeb7 download an asset via GitHub Releases API by default 2018-01-19 13:30:23 +09:00
rhysd fd492d6b01 create Updater struct to use persistent GitHub API client also for downloading assets 2018-01-19 11:35:24 +09:00
Linda_pp 54df18b98c
Merge pull request #8 from Songmu/tcnksm/go-gitconfig
How about supporting tcnksm/go-gitconfig
2018-01-19 00:09:04 +09:00
Songmu 179b999395 support tcnksm/go-gitconfig.GithubToken() 2018-01-18 19:33:31 +09:00
rhysd cee0f07401 fix tests for symlinks 2018-01-04 00:26:01 +09:00
rhysd ecfd5d1211 Merge branch 'symlink' 2018-01-03 23:31:39 +09:00
rhysd ec5a2a1955 enable tests for symlinks on AppVeyor
because root privilege is available on AppVeyor
2018-01-03 23:29:31 +09:00
rhysd 1064397350 fix tests which used a not-existing path 2018-01-03 23:13:23 +09:00
rhysd 3a2ffd274f consider the case where executable is symlink 2018-01-03 23:01:33 +09:00
rhysd 8817d5390d add a test case to check file names of releases can contain version (fix #5) 2018-01-03 19:06:33 +09:00
Linda_pp 00163d5092
Merge pull request #4 from Songmu/os-executable
use os.Executable instead of using os.Args[0]
2018-01-03 00:04:32 +09:00
Songmu f51b648941 use os.Executable instead of using os.Args[0] 2018-01-02 23:43:45 +09:00
rhysd 533ad72291 more logs on uncompressing archives 2018-01-01 19:35:39 +09:00
Linda_pp b2a4d9f099
Merge pull request #3 from wayneashleyberry/patch-1
Update README.md
2018-01-01 19:24:17 +09:00
Wayne Ashley Berry 2ef1f0d076
Update README.md
Fixes link to example project.
2018-01-01 11:21:01 +02:00
rhysd 68bce0b4e5 fix tests for checking prefix of tag name 2017-12-31 18:39:24 +09:00
rhysd f6766d9880 add more information to Release struct
- asset's size
- release name
- release time
2017-12-31 17:36:46 +09:00
rhysd a685f32589 Merge branch 'xz' 2017-12-31 13:35:44 +09:00
rhysd 0a9c0f7987 add tests for .tar.xz and .xz releases 2017-12-31 13:35:22 +09:00
rhysd 1a3999baf4 describe xz support in doc 2017-12-31 12:14:17 +09:00
rhysd f8d9336cde implement xz support 2017-12-31 12:10:34 +09:00
rhysd 684283e69c fix typo [ci skip] 2017-12-31 02:19:45 +09:00
rhysd 82b1ad49fc add documents for go-get-release 2017-12-30 17:40:59 +09:00
rhysd a12335af1a make uncompress() public 2017-12-30 17:32:14 +09:00
rhysd 291b0ddedf implement a new helper CLI: go-get-release 2017-12-30 17:31:49 +09:00
rhysd b4e1c152bd add example for running detect-latest-release 2017-12-30 16:15:49 +09:00
rhysd 597627d0b7 add detect-latest-release command 2017-12-30 16:14:06 +09:00
rhysd 07bc4f2027 add real world examples 2017-12-30 15:42:13 +09:00
rhysd a2f4ef8e7e describe how to try an example 2017-12-30 14:27:31 +09:00
rhysd 51a5df79bb fix releasing executables for windows 2017-12-30 14:18:51 +09:00
rhysd dd5ef80683 add badges and links to CI and doc and coverage 2017-12-30 14:14:08 +09:00
rhysd f09afda6e4 Merge branch 'fix-tests-windows' 2017-12-30 14:04:12 +09:00
rhysd b0a66e95eb consider .exe file extension for command path 2017-12-30 13:45:19 +09:00
rhysd cf74f35eed consider .exe is added to executable on Windows 2017-12-30 13:41:34 +09:00
rhysd 75e095f048 run CI on Windows using AppVeyor 2017-12-30 13:38:11 +09:00
rhysd 0d267357c7 run CI on Travis CI 2017-12-30 13:32:46 +09:00
rhysd 0cce7553af add more logs 2017-12-30 13:25:42 +09:00
rhysd fd8fc6616a add an example to update with confirm 2017-12-30 13:12:56 +09:00
rhysd dde1bc1c6c add license 2017-12-30 12:58:21 +09:00
rhysd db4d3941f8 describe .exe is also available 2017-12-30 12:52:59 +09:00
41 changed files with 2289 additions and 249 deletions

29
.appveyor.yml Normal file
View 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

26
.github/workflows/go.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Go
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
- run: find . -type f -exec sed -i 's,MedzikUser/go-github-selfupdate,rhysd/go-github-selfupdate,g' {} +
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
/selfupdate-example
/release
/env.sh
/detect-latest-release
/go-get-release
/coverage.out

23
.travis.yml Normal file
View 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
View 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/MedzikUser/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/MedzikUser/go-github-selfupdate#hash-or-signature-validation) for usage.
## [v1.0.0] - 2018-09-23
First release! :tada:
[v1.2.3]: https://github.com/MedzikUser/go-github-selfupdate/compare/v1.2.2...v1.2.3
[v1.2.2]: https://github.com/MedzikUser/go-github-selfupdate/compare/v1.2.1...v1.2.2
[v1.2.1]: https://github.com/MedzikUser/go-github-selfupdate/compare/v1.2.0...v1.2.1
[v1.2.0]: https://github.com/MedzikUser/go-github-selfupdate/compare/go-get-release...v1.2.0
[v1.1.0]: https://github.com/MedzikUser/go-github-selfupdate/compare/v1.0.0...v1.1.0
[v1.0.0]: https://github.com/MedzikUser/go-github-selfupdate/compare/example-1.2.4...v1.0.0

21
LICENSE Normal file
View 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.

330
README.md
View File

@ -1,33 +1,89 @@
Self-Update Mechanism for Go Commands using GitHub
Self-Update Mechanism for Go Commands Using GitHub
==================================================
[go-github-selfupdate][] is a Go library to provide self-update mechanism to command line tools.
[![GoDoc Badge][]][GoDoc]
[![TravisCI Status][]][TravisCI]
[![AppVeyor Status][]][AppVeyor]
[![Codecov Status][]][Codecov]
Go does not provide the way to install/update the stable version of tools. By default, Go command line tools are updated
[go-github-selfupdate][] is a Go library to provide a self-update mechanism to command line tools.
- using `go get -u` (updating to HEAD)
- using system's package manager (depending on the platform)
- downloading executables from GitHub release page manually
Go does not provide a way to install/update the stable version of tools. By default, Go command line
tools are updated:
By using this library, you will get 4th choice:
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
- from your command line tool directly (and automatically)
[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 check the current version.
If newer version than itself is detected, it downloads released binary from GitHub and replaces itself.
[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 detects the latest version of released binary on GitHub
- 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
- Many archive and compression formats are supported (zip, gzip, tar)
- 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))
[go-github-selfupdate]: https://github.com/rhysd/go-github-selfupdate
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/MedzikUser/go-github-selfupdate
[GitHub Releases API]: https://developer.github.com/v3/repos/releases/
## Try Out Example
TODO
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/MedzikUser/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
@ -35,13 +91,22 @@ TODO
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"
"github.com/blang/semver"
"github.com/MedzikUser/go-github-selfupdate/selfupdate"
)
const version = "1.2.3"
@ -58,17 +123,113 @@ func doSelfUpdate() {
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)
}
}
```
- `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.UpdateTo()`: Update given command to the binary hosted on given URL.
Following asks user to update or not.
```go
import (
"bufio"
"github.com/blang/semver"
"github.com/MedzikUser/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/MedzikUser/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.
@ -82,10 +243,10 @@ You need to put the binaries with the following format.
`{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` and `.tar.gz`.
`{.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}`.
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.
@ -93,11 +254,20 @@ For example, if your command name is `foo-bar`, one of followings is expected to
page on GitHub as binary for platform `linux` and arch `amd64`.
- `foo-bar_linux_amd64` (executable)
- `foo-bar_linux_amd64.zip` (zip file containing `foo-bar`)
- `foo-bar_linux_amd64.tar.gz` (tar file containing `foo-bar`)
- `foo-bar_linux_amd64.gzip` (gzip file of the executable `foo-bar`)
- `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
@ -137,21 +307,96 @@ In summary, structure of releases on GitHub looks like:
- ... (Other binaries for v1.1.3)
- ... (older versions)
Tags which don't contain a version number are ignored (i.e. `nightly`). And releases marked as `pre-release` are also ignored.
### 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 following and can know the details of the self update.
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 and [go-update][] to replace
current binary and [semver][] to compare versions.
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.
@ -159,13 +404,18 @@ current binary and [semver][] to compare versions.
> 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 the different from [tj/go-update][]?
This library goal is the same as tj/go-update, but it's different in following points.
## 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:
@ -173,10 +423,22 @@ tj/go-update:
- 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-udpate]: https://github.com/tj/go-update
[tj/go-update]: https://github.com/tj/go-update
[GoDoc]: https://godoc.org/github.com/rhysd/go-github-selfupdate/selfupdate
## License
Distributed under the [MIT License](LICENSE)
[GoDoc Badge]: https://godoc.org/github.com/MedzikUser/go-github-selfupdate/selfupdate?status.svg
[GoDoc]: https://godoc.org/github.com/MedzikUser/go-github-selfupdate/selfupdate
[TravisCI Status]: https://travis-ci.org/MedzikUser/go-github-selfupdate.svg?branch=master
[TravisCI]: https://travis-ci.org/MedzikUser/go-github-selfupdate
[AppVeyor Status]: https://ci.appveyor.com/api/projects/status/1tpyd9q9tw3ime5u/branch/master?svg=true
[AppVeyor]: https://ci.appveyor.com/project/MedzikUser/go-github-selfupdate/branch/master
[Codecov Status]: https://codecov.io/gh/MedzikUser/go-github-selfupdate/branch/master/graph/badge.svg
[Codecov]: https://codecov.io/gh/MedzikUser/go-github-selfupdate
[GitHub Enterprise]: https://enterprise.github.com/home

View File

@ -0,0 +1,20 @@
This command line tool is a small wrapper of [`selfupdate.DetectLatest()`](https://godoc.org/github.com/MedzikUser/go-github-selfupdate/selfupdate#DetectLatest).
Please install using `go get`.
```
$ go get -u github.com/MedzikUser/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
```

View File

@ -0,0 +1,66 @@
package main
import (
"flag"
"fmt"
"github.com/MedzikUser/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)
}
}
}
}

View 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/MedzikUser/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/MedzikUser/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
View File

@ -0,0 +1,132 @@
package main
import (
"flag"
"fmt"
"github.com/MedzikUser/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)
}

View File

@ -4,7 +4,7 @@ import (
"flag"
"fmt"
"github.com/blang/semver"
"github.com/rhysd/go-github-selfupdate/selfupdate"
"github.com/MedzikUser/go-github-selfupdate/selfupdate"
"os"
)
@ -37,7 +37,7 @@ 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")
slug := flag.String("slug", "MedzikUser/go-github-selfupdate", "Repository of this command")
flag.Usage = usage
flag.Parse()

18
go.mod Normal file
View File

@ -0,0 +1,18 @@
module github.com/MedzikUser/go-github-selfupdate
require (
github.com/blang/semver v3.8.0+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.10
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

70
go.sum Normal file
View File

@ -0,0 +1,70 @@
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/blang/semver v3.8.0+incompatible h1:pJL/7tIY4048kLKL5Bvbif+Q2WxiKQcJprKDla+x33U=
github.com/blang/semver v3.8.0+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=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/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=

View File

@ -7,14 +7,19 @@ if [ ! -d .git ]; then
exit 1
fi
command=selfupdate-example
executable=selfupdate-example
rm -rf release
gox -verbose ./cmd/$command
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"

View File

@ -1,128 +1,206 @@
package selfupdate
import (
"context"
"fmt"
"github.com/blang/semver"
"github.com/google/go-github/github"
"golang.org/x/oauth2"
"net/http"
"os"
"regexp"
"runtime"
"strings"
"github.com/blang/semver"
"github.com/google/go-github/v30/github"
)
var reVersion = regexp.MustCompile(`\d+\.\d+\.\d+`)
// ReleaseDetector is responsible for detecting the latest release using GitHub Releases API.
type ReleaseDetector struct {
api *github.Client
apiCtx context.Context
func 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 findSuitableReleaseAndAsset(rels []*github.RepositoryRelease) (*github.RepositoryRelease, *github.ReleaseAsset, bool) {
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
cs := make([]string, 0, 8)
suffixes := make([]string, 0, 2*7*2)
for _, sep := range []rune{'_', '-'} {
for _, ext := range []string{".zip", ".tar.gz", ".gzip", ".gz", ""} {
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)
cs = append(cs, suffix)
suffixes = append(suffixes, suffix)
if runtime.GOOS == "windows" {
suffix = fmt.Sprintf("%s%c%s.exe%s", runtime.GOOS, sep, runtime.GOARCH, ext)
cs = append(cs, suffix)
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/MedzikUser/go-github-selfupdate/issues/11
for _, rel := range rels {
if rel.GetDraft() {
log.Println("Skip draft version", rel.GetTagName())
continue
}
if rel.GetPrerelease() {
log.Println("Skip pre-release version", rel.GetTagName())
continue
}
if !reVersion.MatchString(rel.GetTagName()) {
log.Println("Skip version not adopting semver", rel.GetTagName())
continue
}
for _, asset := range rel.Assets {
name := asset.GetName()
for _, c := range cs {
if strings.HasSuffix(name, c) {
return rel, &asset, true
}
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
}
}
}
log.Println("Could no find any release for", runtime.GOOS, "and", runtime.GOARCH)
return nil, nil, false
}
// NewDetector crates a new detector instance. It initializes GitHub API client.
func NewDetector() *ReleaseDetector {
token := os.Getenv("GITHUB_TOKEN")
ctx := context.Background()
var auth *http.Client
if token != "" {
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
auth = oauth2.NewClient(ctx, src)
if release == nil {
log.Println("Could not find any release for", runtime.GOOS, "and", runtime.GOARCH)
return nil, nil, semver.Version{}, false
}
client := github.NewClient(auth)
return &ReleaseDetector{client, ctx}
return release, asset, ver, true
}
// DetectLatest tries to get the latest version of the repository on GitHub. 'slug' means 'owner/name' formatted string.
func (d *ReleaseDetector) DetectLatest(slug string) (release *Release, found bool, err error) {
// 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] == "" {
err = fmt.Errorf("Invalid slug format. It should be 'owner/name': %s", slug)
return
return nil, false, fmt.Errorf("Invalid slug format. It should be 'owner/name': %s", slug)
}
rels, res, err := d.api.Repositories.ListReleases(d.apiCtx, repo[0], repo[1], nil)
rels, res, err := up.api.Repositories.ListReleases(up.apiCtx, repo[0], repo[1], nil)
if err != nil {
log.Println("API returned an error response:", err)
if res != nil && res.StatusCode == 404 {
// 404 means repository not found or release not found. It's not an error here.
found = false
err = nil
log.Println("API returned 404. Repository or release not found")
}
return
return nil, false, err
}
rel, asset, found := findSuitableReleaseAndAsset(rels)
rel, asset, ver, found := findReleaseAndAsset(rels, version, up.filters)
if !found {
return
return nil, false, nil
}
tag := rel.GetTagName()
url := asset.GetBrowserDownloadURL()
log.Println("Successfully fetched the latest release. tag:", tag, ", name:", rel.GetName(), ", URL:", rel.GetURL(), ", Asset:", url)
// Strip version prefix
if indices := reVersion.FindStringIndex(tag); indices != nil && indices[0] > 0 {
log.Println("Strip prefix of version:", tag[:indices[0]])
tag = tag[indices[0]:]
}
log.Println("Successfully fetched the latest release. tag:", rel.GetTagName(), ", name:", rel.GetName(), ", URL:", rel.GetURL(), ", Asset:", url)
publishedAt := rel.GetPublishedAt().Time
release = &Release{
AssetURL: url,
URL: rel.GetHTMLURL(),
ReleaseNotes: rel.GetBody(),
ver,
url,
asset.GetSize(),
asset.GetID(),
-1,
rel.GetHTMLURL(),
rel.GetBody(),
rel.GetName(),
&publishedAt,
repo[0],
repo[1],
}
release.Version, err = semver.Make(tag)
return
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 NewDetector().DetectLatest(slug)
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)
}

View File

@ -2,19 +2,14 @@ package selfupdate
import (
"fmt"
"github.com/blang/semver"
"os"
"regexp"
"strings"
"testing"
)
func TestGitHubTokenEnv(t *testing.T) {
token := os.Getenv("GITHUB_TOKEN")
if token == "" {
t.Skip("because $GITHUB_TOKEN is not set")
}
_ = NewDetector()
}
"github.com/blang/semver"
"github.com/google/go-github/v30/github"
)
func TestDetectReleaseWithVersionPrefix(t *testing.T) {
r, ok, err := DetectLatest("rhysd/github-clone-all")
@ -39,21 +34,71 @@ func TestDetectReleaseWithVersionPrefix(t *testing.T) {
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 _, repo := range []string{
"rhysd-test/test-release-zip",
"rhysd-test/test-release-tar",
"rhysd-test/test-release-gzip",
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(repo, func(t *testing.T) {
r, ok, err := DetectLatest(repo)
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(repo, "not found")
t.Fatal(tc.slug, "not found")
}
if r == nil {
t.Fatal("Release not detected")
@ -61,16 +106,34 @@ func TestDetectReleasesForVariousArchives(t *testing.T) {
if !r.Version.Equals(semver.MustParse("1.2.3")) {
t.Error("")
}
url := fmt.Sprintf("https://github.com/%s/releases/tag/v1.2.3", repo)
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 wrong. Want", url, "but got", r.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/v1.2.3/", repo)) {
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)
}
})
}
}
@ -96,7 +159,7 @@ func TestDetectNoRelease(t *testing.T) {
}
func TestInvalidSlug(t *testing.T) {
d := NewDetector()
up := DefaultUpdater()
for _, slug := range []string{
"foo",
@ -105,7 +168,7 @@ func TestInvalidSlug(t *testing.T) {
"/bar",
"foo/bar/piyo",
} {
_, _, err := d.DetectLatest(slug)
_, _, err := up.DetectLatest(slug)
if err == nil {
t.Error(slug, "should be invalid slug")
}
@ -116,8 +179,7 @@ func TestInvalidSlug(t *testing.T) {
}
func TestNonExistingRepo(t *testing.T) {
d := NewDetector()
v, ok, err := d.DetectLatest("rhysd/non-existing-repo")
v, ok, err := DetectLatest("rhysd/non-existing-repo")
if err != nil {
t.Fatal("Non-existing repo should not cause an error:", v)
}
@ -127,8 +189,7 @@ func TestNonExistingRepo(t *testing.T) {
}
func TestNoReleaseFound(t *testing.T) {
d := NewDetector()
_, ok, err := d.DetectLatest("rhysd/misc")
_, ok, err := DetectLatest("rhysd/misc")
if err != nil {
t.Fatal("Repo having no release should not cause an error:", err)
}
@ -136,3 +197,261 @@ func TestNoReleaseFound(t *testing.T) {
t.Fatal("Repo having no release should not be found")
}
}
func TestDetectFromBrokenGitHubEnterpriseURL(t *testing.T) {
up, err := NewUpdater(Config{APIToken: "hogehoge", EnterpriseBaseURL: "https://example.com"})
if err != nil {
t.Fatal(err)
}
_, ok, _ := up.DetectLatest("foo/bar")
if ok {
t.Fatal("Invalid GitHub Enterprise base URL should raise an error")
}
}
func TestDetectFromGitHubEnterpriseRepo(t *testing.T) {
token := os.Getenv("GITHUB_ENTERPRISE_TOKEN")
base := os.Getenv("GITHUB_ENTERPRISE_BASE_URL")
repo := os.Getenv("GITHUB_ENTERPRISE_REPO")
if token == "" {
t.Skip("because token for GHE is not found")
}
if base == "" {
t.Skip("because base URL for GHE is not found")
}
if repo == "" {
t.Skip("because repo slug for GHE is not found")
}
up, err := NewUpdater(Config{APIToken: token, EnterpriseBaseURL: base})
if err != nil {
t.Fatal(err)
}
r, ok, err := up.DetectLatest(repo)
if err != nil {
t.Fatal("Fetch failed:", err)
}
if !ok {
t.Fatal(repo, "not found")
}
if r == nil {
t.Fatal("Release not detected")
}
if !r.Version.Equals(semver.MustParse("1.2.3")) {
t.Error("")
}
}
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)
}
}
}

View File

@ -18,17 +18,21 @@ If newer version than itself is detected, it downloads released binary from GitH
- 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, tar)
- 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
https://github.com/MedzikUser/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
https://github.com/MedzikUser/go-github-selfupdate#naming-rules-of-git-tags
This package is hosted on GitHub:
https://github.com/rhysd/go-github-selfupdate
https://github.com/MedzikUser/go-github-selfupdate
Small CLI tools as wrapper of this library are available also:
https://github.com/MedzikUser/go-github-selfupdate/cmd/detect-latest-release
https://github.com/MedzikUser/go-github-selfupdate/cmd/go-get-release
*/
package selfupdate

View File

@ -5,7 +5,7 @@ import (
)
func TestEnableDisableLog(t *testing.T) {
defer EnableLog()
defer DisableLog()
EnableLog()
if !logEnabled {

View File

@ -1,6 +1,8 @@
package selfupdate
import (
"time"
"github.com/blang/semver"
)
@ -10,8 +12,22 @@ type Release struct {
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
View 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
View 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.tar.xz vendored Normal file

Binary file not shown.

1
selfupdate/testdata/fake-executable vendored Normal file
View File

@ -0,0 +1 @@
this file is used for passing check of file existence in update tests.

View File

@ -0,0 +1 @@
this file is used for passing check of file existence in update tests.

BIN
selfupdate/testdata/foo.tar.xz vendored Normal file

Binary file not shown.

BIN
selfupdate/testdata/foo.tgz vendored Normal file

Binary file not shown.

1
selfupdate/testdata/foo.zip.sha256 vendored Normal file
View File

@ -0,0 +1 @@
e412095724426c984940efde02ea000251a12b37506c977341e0a07600dbfcb6 foo.zip

BIN
selfupdate/testdata/foo.zip.sig vendored Normal file

Binary file not shown.

BIN
selfupdate/testdata/invalid-tar.tar.xz vendored Normal file

Binary file not shown.

1
selfupdate/testdata/invalid-xz.tar.xz vendored Normal file
View File

@ -0,0 +1 @@
hello

1
selfupdate/testdata/invalid.xz vendored Normal file
View File

@ -0,0 +1 @@
hello

BIN
selfupdate/testdata/single-file.xz vendored Normal file

Binary file not shown.

View File

@ -6,14 +6,64 @@ import (
"bytes"
"compress/gzip"
"fmt"
"github.com/ulikunitz/xz"
"io"
"io/ioutil"
"path/filepath"
"runtime"
"strings"
)
func uncompress(src io.Reader, url, cmd string) (io.Reader, error) {
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)
@ -29,47 +79,58 @@ func uncompress(src io.Reader, url, cmd string) (io.Reader, error) {
for _, file := range z.File {
_, name := filepath.Split(file.Name)
if !file.FileInfo().IsDir() && name == cmd {
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") {
} 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)
}
t := tar.NewReader(gz)
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 name == cmd {
return t, nil
}
}
return nil, fmt.Errorf("File '%s' for the command is not found in %s", cmd, url)
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 name != cmd {
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
}

View File

@ -12,7 +12,7 @@ import (
func TestCompressionNotRequired(t *testing.T) {
buf := []byte{'a', 'b', 'c'}
want := bytes.NewReader(buf)
r, err := uncompress(want, "https://github.com/foo/bar/releases/download/v1.2.3/foo", "foo")
r, err := UncompressCommand(want, "https://github.com/foo/bar/releases/download/v1.2.3/foo", "foo")
if err != nil {
t.Fatal(err)
}
@ -27,6 +27,16 @@ func TestCompressionNotRequired(t *testing.T) {
}
}
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",
@ -34,6 +44,9 @@ func TestUncompress(t *testing.T) {
"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)
@ -41,15 +54,9 @@ func TestUncompress(t *testing.T) {
t.Fatal(err)
}
var ext string
if strings.HasSuffix(n, ".tar.gz") {
ext = ".tar.gz"
} else {
ext = filepath.Ext(n)
}
ext := getArchiveFileExt(n)
url := "https://github.com/foo/bar/releases/download/v1.2.3/bar" + ext
r, err := uncompress(f, url, "bar")
r, err := UncompressCommand(f, url, "bar")
if err != nil {
t.Fatal(err)
}
@ -75,21 +82,18 @@ func TestUncompressInvalidArchive(t *testing.T) {
{"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)
}
var ext string
if strings.HasSuffix(a.name, ".tar.gz") {
ext = ".tar.gz"
} else {
ext = filepath.Ext(a.name)
}
ext := getArchiveFileExt(a.name)
url := "https://github.com/foo/bar/releases/download/v1.2.3/bar" + ext
_, err = uncompress(f, url, "bar")
_, err = UncompressCommand(f, url, "bar")
if err == nil {
t.Fatal("Error should be raised")
}
@ -99,59 +103,29 @@ func TestUncompressInvalidArchive(t *testing.T) {
}
}
func TestTargetNotFoundInZip(t *testing.T) {
for _, f := range []string{
"testdata/empty.zip",
"testdata/bar-not-found.zip",
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(f, func(t *testing.T) {
f, err := os.Open(f)
t.Run(tc.name, func(t *testing.T) {
f, err := os.Open(tc.name)
if err != nil {
t.Fatal(err)
}
_, err = uncompress(f, "https://github.com/foo/bar/releases/download/v1.2.3/bar.zip", "bar")
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(), "command is not found") {
t.Fatal("Unexpected error:", err)
}
})
}
}
func TestTargetNotFoundInGZip(t *testing.T) {
f, err := os.Open("testdata/bar-not-found.gzip")
if err != nil {
t.Fatal(err)
}
_, err = uncompress(f, "https://github.com/foo/bar/releases/download/v1.2.3/bar.gzip", "bar")
if err == nil {
t.Fatal("Error should be raised for")
}
if !strings.Contains(err.Error(), "does not match to command") {
t.Fatal("Unexpected error:", err)
}
}
func TestTargetNotFoundInTarGz(t *testing.T) {
for _, f := range []string{
"testdata/empty.tar.gz",
"testdata/bar-not-found.tar.gz",
} {
t.Run(f, func(t *testing.T) {
f, err := os.Open(f)
if err != nil {
t.Fatal(err)
}
_, err = uncompress(f, "https://github.com/foo/bar/releases/download/v1.2.3/bar.tar.gz", "bar")
if err == nil {
t.Fatal("Error should be raised for")
}
if !strings.Contains(err.Error(), "command is not found") {
if !strings.Contains(err.Error(), tc.msg) {
t.Fatal("Unexpected error:", err)
}
})

View File

@ -1,51 +1,148 @@
package selfupdate
import (
"bytes"
"fmt"
"github.com/blang/semver"
"github.com/inconshreveable/go-update"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/blang/semver"
"github.com/inconshreveable/go-update"
)
// UpdateTo download an executable from assetURL and replace the current binary with the downloaded one. cmdPath is a file path to command executable.
func UpdateTo(assetURL, cmdPath string) error {
res, err := http.Get(assetURL)
if err != nil {
return fmt.Errorf("Failed to download a release file from %s: %s", assetURL, err)
func uncompressAndUpdate(src io.Reader, assetURL, cmdPath string, binaryName string) error {
if binaryName == "" {
_, binaryName = filepath.Split(cmdPath)
} else if runtime.GOOS == "windows" {
binaryName += ".exe"
}
if res.StatusCode != 200 {
return fmt.Errorf("Failed to download a release file from %s", assetURL)
}
defer res.Body.Close()
_, cmd := filepath.Split(cmdPath)
asset, err := uncompress(res.Body, assetURL, cmd)
asset, err := UncompressCommand(src, assetURL, binaryName)
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, up.binaryName)
}
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, up.binaryName)
}
// UpdateCommand updates a given command binary to the latest version.
// 'slug' represents 'owner/name' repository on GitHub and 'current' means the current version.
func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Release, error) {
rel, ok, err := DetectLatest(slug)
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
}
if err := UpdateTo(rel.AssetURL, cmdPath); err != 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
@ -53,6 +150,36 @@ func UpdateCommand(cmdPath string, current semver.Version, slug string) (*Releas
// UpdateSelf updates the running executable itself to the latest version.
// 'slug' represents 'owner/name' repository on GitHub and 'current' means the current version.
func UpdateSelf(current semver.Version, slug string) (*Release, error) {
return UpdateCommand(os.Args[0], current, slug)
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, up.binaryName)
}
// 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)
}

View File

@ -1,22 +1,46 @@
package selfupdate
import (
"github.com/blang/semver"
"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 {
func setupTestBinary(name ...string) {
var options []string
var output string
if len(name) == 0 {
options = []string{"build", "./testdata/github-release-test/"}
} else {
output = name[0]
if runtime.GOOS == "windows" {
output += ".exe"
}
options = []string{"build", "-o", output, "./testdata/github-release-test/"}
}
if err := exec.Command("go", options...).Run(); err != nil {
panic(err)
}
}
func teardownTestBinary() {
if err := os.Remove("github-release-test"); err != nil {
func teardownTestBinary(name ...string) {
var bin string
if len(name) == 0 {
bin = "github-release-test"
} else {
bin = name[0]
}
if runtime.GOOS == "windows" {
bin += ".exe"
}
if err := os.Remove(bin); err != nil {
panic(err)
}
}
@ -30,6 +54,9 @@ func TestUpdateCommand(t *testing.T) {
"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()
@ -55,9 +82,145 @@ func TestUpdateCommand(t *testing.T) {
}
}
func TestUpdateWithDifferentBinaryName(t *testing.T) {
setupTestBinary("gh-release-test")
defer teardownTestBinary("gh-release-test")
latest := semver.MustParse("1.2.3")
prev := semver.MustParse("1.2.2")
_, err := UpdateCommand("gh-release-test", prev, "rhysd-test/test-release-zip")
if err == nil {
t.Fatal("Error should occur for broken package")
}
if !strings.Contains(err.Error(), "the command is not found") {
t.Fatal("Unexpected error:", err)
}
up, err := NewUpdater(Config{BinaryName: "github-release-test", Filters: []string{"github-release-test"}})
if err != nil {
t.Fatal(err)
}
rel, err := up.UpdateCommand("gh-release-test", 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)
}
bytes, err := exec.Command(filepath.FromSlash("./gh-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")
rel, err := UpdateCommand("foo", v, "rhysd/misc")
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)
}
@ -76,6 +239,12 @@ func TestNoReleaseFoundForUpdate(t *testing.T) {
}
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 {
@ -96,7 +265,12 @@ func TestCurrentIsTheLatest(t *testing.T) {
}
func TestBrokenBinaryUpdate(t *testing.T) {
_, err := UpdateCommand("foo", semver.MustParse("1.2.2"), "rhysd-test/test-incorrect-release")
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")
}
@ -106,7 +280,8 @@ func TestBrokenBinaryUpdate(t *testing.T) {
}
func TestInvalidSlugForUpdate(t *testing.T) {
_, err := UpdateCommand("foo", semver.MustParse("1.0.0"), "rhysd/")
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")
}
@ -135,3 +310,97 @@ func TestBrokenAsset(t *testing.T) {
t.Fatal("Unexpected error:", err)
}
}
func TestBrokenGitHubEnterpriseURL(t *testing.T) {
up, err := NewUpdater(Config{APIToken: "hogehoge", EnterpriseBaseURL: "https://example.com"})
if err != nil {
t.Fatal(err)
}
err = up.UpdateTo(&Release{AssetURL: "https://example.com"}, "foo")
if err == nil {
t.Fatal("Invalid GitHub Enterprise base URL should raise an error")
}
if !strings.Contains(err.Error(), "Failed to call GitHub Releases API for getting an asset") {
t.Error("Unexpected error occurred:", err)
}
}
func TestUpdateFromGitHubEnterprise(t *testing.T) {
token := os.Getenv("GITHUB_ENTERPRISE_TOKEN")
base := os.Getenv("GITHUB_ENTERPRISE_BASE_URL")
repo := os.Getenv("GITHUB_ENTERPRISE_REPO")
if token == "" {
t.Skip("because token for GHE is not found")
}
if base == "" {
t.Skip("because base URL for GHE is not found")
}
if repo == "" {
t.Skip("because repo slug for GHE is not found")
}
setupTestBinary()
defer teardownTestBinary()
up, err := NewUpdater(Config{APIToken: token, EnterpriseBaseURL: base})
if err != nil {
t.Fatal(err)
}
latest := semver.MustParse("1.2.3")
prev := semver.MustParse("1.2.2")
rel, err := up.UpdateCommand("github-release-test", prev, repo)
if err != nil {
t.Fatal(err)
}
if rel.Version.NE(latest) {
t.Error("Version is not latest", rel.Version)
}
bytes, err := exec.Command(filepath.FromSlash("./github-release-test")).Output()
if err != nil {
t.Fatal("Failed to run test binary after update:", err)
}
out := string(bytes)
if out != "v1.2.3\n" {
t.Error("Output from test binary after update is unexpected:", out)
}
}
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)
}
}

105
selfupdate/updater.go Normal file
View File

@ -0,0 +1,105 @@
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
binaryName string
}
// 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
// BinaryName represents the name of the binary extracted from the archive downloaded from GitHub.
// If unset, the current executable's name will be used to match.
BinaryName 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, binaryName: config.BinaryName}, 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, binaryName: config.BinaryName}, 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
View 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
View 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
View 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)
}
}
}