LoginSignup
13
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-08

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

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

(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へバイナリがアップロードされ、ユーザ側の操作でその最新のバイナリが適用されるようなフローの実現を目指します。

  1. git tagをpush
  2. Circle CIでビルドしてバイナリをGitHub Releasesにアップロード
  3. ユーザが$ 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ではtestreleaseのジョブを定義し、workflowstestジョブが成功したら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のSettingsDeveloper SettingsPersonal access tokensGenerate 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に更新されます。

まとめ

goreleasergo-github-selfupdateを組み合わせることで、開発者は簡単にツールをリリースし、ユーザは容易にその最新のバージョンへ追従することができるようになりました。
これでツール本体の開発に集中できますね。golangで最高のCLIツールをどんどんリリースしましょう!

13
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
5