golangでCLIを作った後にいつも悩むのが、ユーザへCLIのアップデート手段をどう提供するかです。
macだとbrew update
一発なので楽ですが、LinuxやwindowsだとGitHub Releasesからバイナリをダウンロードしてもらうことになりがちです。また、そもそもリリース自体が簡単に行えるような仕組みが整っていないと、スピーディーにアップデートを提供することができません。
幸いにもgolangではミニマルなツールを組み合わせてこれらを実現すエコシステムが整っています。以下はリリースを行うまでの必要な作業とそれを支援してくれるツールの例です。
- 様々なプラットフォーム用にビルド(mitchellh/gox / laher/goxc / Songmu/goxz)
- ビルドしたバイナリを圧縮(laher/goxc / Songmu/goxz)
- CHANGELOGをアップデート(Songmu/ghch)
- GitHub Releaseへバイナリをアップロード(tcnksm/ghr)
- homebrew用のリポジトリを更新
- GitHub Releasesからバイナリをダウンロードしてツールをアップグレード(rhysd/go-github-selfupdate)
(gored: golangプロジェクトをローカルでもCIでもビルド/リリースできるようにするための環境整備ツール - Qiitaでも紹介していますので、興味があればご覧ください。)
これらのツール群は、それぞれが最小限の責務を担っているのでカスタマイズや入れ替えが容易な反面、一連のフローを実現するために必要なツール数が多いので覚えるのがちょっと大変だったりします。
goreleaser
goreleaserはこれらのミニマルなツールとは真逆の道をゆくもので、上で書いたようなリリースに必要なあらゆる機能がひたすら詰め込まれています。何もしなくてもデフォルトでいい感じにリリースされるようになっていますが、.goreleaser.yml
というyamlを書くことで上書きすることもできます。
go-github-selfupdate
go-github-selfupdateは、はじめに書いたようなツールが更新するたびにユーザがバイナリをダウンロードしてきてパスの通った場所に置く作業を解消してくれるライブラリです。
mitchellh/gox的な命名規則に則ってGitHub Releasesにバイナリを置いておくと、自動でダウンロードし、自分自身をそのバイナリに置き換えることでツールのセルフアップデートを実現しています。詳しくは作者の方の紹介スライド(go-selfupdate-github で ツールを自己アップデートする)をご覧ください。
goreleaser
ではデフォルトでgoxの命名規則に沿ってバイナリをアップロードするので、簡単にgo-github-selfupdate
と組み合わせることができます。
使ってみる
開発者がgit tagをpushするとGitHub Releaseへバイナリがアップロードされ、ユーザ側の操作でその最新のバイナリが適用されるようなフローの実現を目指します。
- git tagをpush
- Circle CIでビルドしてバイナリをGitHub Releasesにアップロード
- ユーザが
$ tool_name selfupdate
を叩くと最新のバイナリに更新される
goreleaser.yml
まずは.goreleaser.yaml
を作りましょう。
builds:
- goos:
- darwin
- linux
- windows
ignore:
- goos: darwin
goarch: 386
archive:
format_overrides:
- goos: windows
format: zip
セクションごとに解説します。
buildセクション
builds:
- goos:
- darwin
- linux
- windows
ignore:
- goos: darwin
goarch: 386
buildセクションでは、その名の通りビルドに関する設定を行います。
goos
はgolangの設定そのまんまですね。ignore
ではビルドしない組み合わせを指定することができます。上の例では32bitのmac用ビルドをスキップします。
archiveセクション
archive:
format_overrides:
- goos: windows
format: zip
archive
セクションでは、ビルド成果物の名前や圧縮方法について設定します。
デフォルトではtar.gz
で圧縮されますが、上の例ではwindowsだけzip圧縮に変更しています。
ちなみに、.goreleaser.yml
を生成するコマンドであるgoreleaser init
を実行するとarchive
には以下のような項目が追加されます。
archive:
replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
replacementsはファイル名に含めるOS名などをデフォルトから変更するセクションですが、こうしてしまうとgo-github-selfupdateが想定するファイル名のフォーマットとズレてしまうので、このセクションは削除してください。
Circle CIの設定
git tagがpushされたらCIのジョブを起動し、自動的にgoreleaserが実行されるようにします。ここではCircle CIを使いますが、Travis CIなど他のCIでも実現可能です。詳細は公式ドキュメント(Continuous Integration)をご覧ください。
まずは、次のymlを.circleci/config.yml
として保存してください。
defaults: &defaults
docker:
- image: circleci/golang:1.11
working_directory: /go/src/github.com/your_name/tool_name
version: 2
jobs:
test:
<<: *defaults
steps:
- checkout
# specify any bash command here prefixed with `run: `
- run: go test ./...
release:
<<: *defaults
steps:
- checkout
- run: curl -sL https://git.io/goreleaser | bash
workflows:
version: 2
test_and_release:
jobs:
- test:
filters:
branches:
only: /.*/
tags:
only: /v[0-9]+(\.[0-9]+)*(-.*)*/
- release:
filters:
branches:
ignore: /.*/
tags:
only: /v[0-9]+(\.[0-9]+)*(-.*)*/
requires:
- test
jobs
jobs
ではtest
とrelease
のジョブを定義し、workflows
でtest
ジョブが成功したらrelease
ジョブを実行するようにしています。
test
ジョブはその名の通りgo testを行うジョブです。
ここでは簡略化のために省いていますが、実際はdepなどのパッケージマネージャを利用している場合はその実行も必要です。
release
ジョブはgoreleaserを使ってリリースを行うジョブです。
goreleaserはあらかじめバイナリをインストールしておかなくても、同様の処理を実行してくれるシェルスクリプトをhttps://git.io/goreleaser
で提供しています。そのため、ここではgoreleaser
コマンドを実行するのではなくcurl -sL https://git.io/goreleaser | bash
で同じ処理を実現しています。
workflows
workflows
では、filter
でジョブを実行する条件を、require
で依存するジョブの定義を行うことができます。
ここでのポイントは、test
ジョブにfilter
としてtags
の設定を入れている部分です。
Circle CIではbranchesやtagsで、対象とするブランチやタグを明示的に指定しないと実行されません。
release
ジョブではv0.0.0
のようなバージョンを表すtagがpushされたときにだけ実行したいので、 only: /v[0-9]+(\.[0-9]+)*(-.*)*/
というような指定にしています。
しかし、release
ジョブはtest
ジョブをrequireしているため、test
ジョブもtagのpush時に実行される
必要があります。
というわけでtestジョブにも同様の設定を追加しています。
最後に、goreleaserでGitHub Releaseへのバイナリアップロードを行うためには、GITHUB_TOKEN
という環境変数にトークンを設定しておく必要があります。
GitHubのSettings
→Developer Settings
→Personal access tokens
→Generate new token
でトークンを生成し、Circle CIのEnvironment Variables
へ設定しましょう。
これで、例えばv0.0.2
というgit tagをpushすると自動的にGitHub Releaseとhomebrew用リポジトリへv0.0.2
としてリリースされるようになりました。
selfupdate用のコードを記述
go-github-selfupdate
では、selfupdate.UpdateSelf
メソッドを実行するだけで、現在実行中のバイナリを最新版に置き換えてくれます。
以下はspf13/cobra:を利用して、selfupdate
コマンドを実装する例です。(main.go
などは省略しています。)
package lib
const Version = "1.2.3"
const slug = "your_name/tool_name"
func DoSelfUpdate() (bool, error) {
v := semver.MustParse(Version)
latest, err := selfupdate.UpdateSelf(v, slug)
return !latest.Version.Equals(v), errors.Wrap(err, "Binary update failed")
}
package cmd
var selfUpdateCmd = &cobra.Command{
Use: "selfupdate",
Short: "update this tool",
Run: func(cmd *cobra.Command, args []string) {
updated, err := lib.DoSelfUpdate()
if err != nil {
log.Println(err)
return
}
if updated {
log.Println("Current binary is the latest version", lib.Version)
} else {
log.Println("Successfully updated to version", lib.Version)
}
},
}
func init() {
rootCmd.AddCommand(selfUpdateCmd)
}
実装はこれだけです。
これで、例えばユーザがv0.0.1
を利用している状態でGitHub Releasesにv0.0.2
のリリースが存在する場合、$ tool_name selfupdate
を実行すると、ユーザのツールがv0.0.2
に更新されます。
まとめ
goreleaser
とgo-github-selfupdate
を組み合わせることで、開発者は簡単にツールをリリースし、ユーザは容易にその最新のバージョンへ追従することができるようになりました。
これでツール本体の開発に集中できますね。golangで最高のCLIツールをどんどんリリースしましょう!