goreleaser+go-github-selfupdateでお手軽自動リリース&アップデート

golangでCLIを作った後にいつも悩むのが、ユーザへCLIのアップデート手段をどう提供するかです。
macだと`brew update`一発なので楽ですが、LinuxやwindowsだとGitHub Releasesからバイナリをダウンロードしてもらうことになりがちです。また、そもそもリリース自体が簡単に行えるような仕組みが整っていないと、スピーディーにアップデートを提供することができません。

幸いにもgolangではミニマルなツールを組み合わせてこれらを実現すエコシステムが整っています。以下はリリースを行うまでの必要な作業とそれを支援してくれるツールの例です。

- 様々なプラットフォーム用にビルド([mitchellh/gox](https://github.com/mitchellh/gox) / [laher/goxc](https://github.com/laher/goxc) / [Songmu/goxz](https://github.com/Songmu/goxz))
- ビルドしたバイナリを圧縮([laher/goxc](https://github.com/laher/goxc) / [Songmu/goxz](https://github.com/Songmu/goxz))
- CHANGELOGをアップデート([Songmu/ghch](https://github.com/Songmu/ghch))
- GitHub Releaseへバイナリをアップロード([tcnksm/ghr](https://github.com/tcnksm/ghr))
- homebrew用のリポジトリを更新
- GitHub Releasesからバイナリをダウンロードしてツールをアップグレード([rhysd/go\-github\-selfupdate](https://github.com/rhysd/go-github-selfupdate))

([gored: golangプロジェクトをローカルでもCIでもビルド/リリースできるようにするための環境整備ツール \- Qiita](https://qiita.com/mpppk/items/c59d03854ce89de3e539)でも紹介していますので、興味があればご覧ください。)

これらのツール群は、それぞれが最小限の責務を担っているのでカスタマイズや入れ替えが容易な反面、一連のフローを実現するために必要なツール数が多いので覚えるのがちょっと大変だったりします。

## goreleaser

[goreleaser](https://goreleaser.com/)はこれらのミニマルなツールとは真逆の道をゆくもので、上で書いたようなリリースに必要なあらゆる機能がひたすら詰め込まれています。何もしなくてもデフォルトでいい感じにリリースされるようになっていますが、`.goreleaser.yml`というyamlを書くことで上書きすることもできます。

## go-github-selfupdate
[go-github-selfupdate](https://github.com/rhysd/go-github-selfupdate)は、はじめに書いたようなツールが更新するたびにユーザがバイナリをダウンロードしてきてパスの通った場所に置く作業を解消してくれるライブラリです。
[mitchellh/gox](https://github.com/mitchellh/gox)的な命名規則に則ってGitHub Releasesにバイナリを置いておくと、自動でダウンロードし、自分自身をそのバイナリに置き換えることでツールのセルフアップデートを実現しています。詳しくは[作者の方の紹介スライド(go\-selfupdate\-github で ツールを自己アップデートする)](https://speakerdeck.com/rhysd/go-selfupdate-github-de-turuwozi-ji-atupudetosuru)をご覧ください。
`goreleaser`ではデフォルトでgoxの命名規則に沿ってバイナリをアップロードするので、簡単に`go-github-selfupdate`と組み合わせることができます。

## 使ってみる
開発者がgit tagをpushするとGitHub Releaseへバイナリがアップロードされ、ユーザ側の操作でその最新のバイナリが適用されるようなフローの実現を目指します。

1. git tagをpush
2. Circle CIでビルドしてバイナリをGitHub Releasesにアップロード
4. ユーザが`$ tool_name selfupdate`を叩くと最新のバイナリに更新される

### goreleaser.yml

まずは`.goreleaser.yaml`を作りましょう。

```yml
builds:
  - goos:
      - darwin
      - linux
      - windows
    ignore:
      - goos: darwin
        goarch: 386
archive:
  format_overrides:
    - goos: windows
      format: zip
```

セクションごとに解説します。

#### buildセクション

```yml
builds:
  - goos:
      - darwin
      - linux
      - windows
    ignore:
      - goos: darwin
        goarch: 386
```

buildセクションでは、その名の通りビルドに関する設定を行います。
`goos`はgolangの設定そのまんまですね。`ignore`ではビルドしない組み合わせを指定することができます。上の例では32bitのmac用ビルドをスキップします。

#### archiveセクション

```yaml
archive:
  format_overrides:
    - goos: windows
      format: zip
```

`archive`セクションでは、ビルド成果物の名前や圧縮方法について設定します。
デフォルトでは`tar.gz`で圧縮されますが、上の例ではwindowsだけzip圧縮に変更しています。
ちなみに、`.goreleaser.yml`を生成するコマンドである`goreleaser init`を実行すると`archive`には以下のような項目が追加されます。

```yaml
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)](https://goreleaser.com/ci/)をご覧ください。

まずは、次のymlを`.circleci/config.yml`として保存してください。

```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:](https://github.com/spf13/cobra)を利用して、`selfupdate`コマンドを実装する例です。(`main.go`などは省略しています。)

```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")
}
```

```go
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ツールをどんどんリリースしましょう！