michimani.net

Go で書いた CLI ツールを GitHub Actions と GoReleaser を使って brew コマンドでインストールできるようにした

2021-02-05

Go でちょっとした CLI ツールを作ったので、 GitHub Actions と GoReleaser を使って brew コマンドでインストールできるようにしてみました。その時の手順をまとめておきます。

目次

概要

GitHub Actions と GoReleaser を使って Go で書いた CLI ツールを brew コマンドでインストールできるようにします。

実際にこの方法で公開しているツールがあるので、これを公開するときにとった手順を書いていきます。

michimani/genlink - GitHub

ちなみにこれは、 URL を渡すといろんな形式 (Markdown/HTML/QR コード) でリンクを生成してくれるツールです。

やったこと

やったことは下記の通りです。

  1. homebrew-genlink リポジトリを作成
  2. genlink リポジトリを作成
  3. CLI ツールの実装
  4. CREDITS ファイルを作成
  5. .github/workflows/release.yml.goreleaser.yml を作成
  6. タグ付けして Push
  7. brew コマンドでインストール

それぞれ詳細を書いていきます。

まずは GitHub で homebrew-genlink というリポジトリを作ります。

これは、作成したツールを brew tap を使用して配布するために必要なリポジトリとなります。

brew コマンドでインストールできるツールは、基本的には Homebrew 公式の Homebrew/homebrew-core に追加されている必要があります。ただし、公式リポジトリへ追加してもらうとなると、 PR を作ってレビューしてもらってマージしてもらう流れになり、公開までに時間がかかります。また、バージョンアップのたびにこのフローを踏む必要があります。

brew tap を使うと、公式以外のリポジトリにあるツールを brew コマンドでインストールできるようになります。その際、対象となるリポジトリの命名規則が「homebrew-{hoge}」となっています。

このリポジトリはあくまでも brew tap で認識させるためのリポジトリで、このリポジトリでソースコードを管理するわけではありません。

brew/Taps.md at master · Homebrew/brew

実際にソースコードを管理するリポジトリ genlink を作成します。リポジトリを作成する際、 LICENSE を一緒に作っておきます。(これは任意です)

2-1. CLI ツールの実装

ツールを実装します。

Go で CLI ツールを作成する際、標準ライブラリの flag パッケージを使用することでコマンド実行時のパラメータをいい感じに扱う事ができます。 main.go はこんな感じになってます。

package main

import (
	"flag"
	"fmt"
	"os"
)

var (
	targetUrl *string = flag.String("u", "", "Type of link to output")
	genType   *string = flag.String("t", "md", "Type of link to output")
	outDir    *string = flag.String("o", "", "Directory to output QR code")
	version   string
	revision  string
)

func usage() {
	format := `
                  _ _       _
  __ _  ___ _ __ | (_)_ __ | | __
 / _' |/ _ \ '_ \| | | '_ \| |/ /
| (_| |  __/ | | | | | | | |   <
 \__, |\___|_| |_|_|_|_| |_|_|\_\
 |___/   Version: %s-%s

Usage:
  genlink [flags] [values]
Flags:
	-u (required)  URL
	-t             Type of link to output
	    md:        Markdown (default)
	    html:      HTML a tag
	    html-bl:   HTML a tag with 'target="_blank"'
	    qr:        QR code image
	-o             Absolute path to directory to output QR code
	               Use this flag in combination with '-t qr'
	               Default is current directory

Author:
  michimani <michimani210@gmail.com>
`
	fmt.Fprintln(os.Stderr, fmt.Sprintf(format, version, revision))
}

func main() {
	flag.Usage = usage
	flag.Parse()

	os.Exit(run())
}

func run() int {
	res, err := Generate(*targetUrl, *genType, *outDir)
	if err != nil {
		fmt.Println(err.Error())
		return 1
	}

	fmt.Println(res)

	return 0
}

flag.Usage に代入した関数は、 -h または --help オプションを付けて実行した際に実行されるので、コマンドの使い方を出力するようにしています。

	version   string
	revision  string

宣言だけして初期化していないこの変数については後で説明します。

ちなみに 「genlink」アスキーアートは figlet コマンドで生成しました。 macOS であれば brew install figlet でインストールできます。

2-2. CREDITS ファイルを作成

Go で作成した (Go に限らずですが) ツールを配布する際、そのツール内で使用したパッケージやライブラリのライセンス情報をバイナリに同梱する必要があります。Go の場合、サードパーティのパッケージだけでなく標準パッケージについてもライセンス情報を含める必要があります。これについてはこのあたりで議論されています。

Standard library licensing question · Issue #19893 · golang/go

ただ、使用しているパッケージのライセンス情報をすべて確認するのは面倒です。そこで便利なのが、 gocredits というツールです。

Songmu/gocredits: creates CREDITS file from LICENSE files of dependencies

brew または go get でインストールし、プロジェクトのルートディレクトリで gocredits . > CREDITS を実行するだけで依存パッケージのライセンス情報が CREDITS ファイルに出力されます。

2-3. .github/workflows/release.yml.goreleaser.yml を作成

.github/workflows/release.yml は GitHub Actions を起動するためのファイルです。これについては GoReleaser のドキュメント内にサンプルがあり、ほぼそのまま使用することができます。

GitHub Actions - GoReleaser

今回はこのサンプルを下記のように変更しています。

name: release
on:
  push:
    tags:
      - "v[0-9]+.[0-9]+.[0-9]+"
jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 1
      - name: Setup Go
        uses: actions/setup-go@v2
        with:
          go-version: 1.15
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v2
        with:
          version: latest
          args: release --rm-dist
        env:
          GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}

サンプルからの主な変更点を書いておきます。

まずは、GitHub Actions の起動条件を、tag が Push されたときにしています。

on:
  push:
    tags:
      - "v[0-9]+.[0-9]+.[0-9]+"

続いて、 GITHUB_TOKEN に設定する値の名前を secrets.ACCESS_TOKEN に変更しています。これは GITHUB_TOKEN のまま実行した際に Actions で下記のようなエラーになったからです。

   ⨯ release failed after 45.53s error=homebrew tap formula: failed to publish artifacts: PUT https://api.github.com/repos/michimani/homebrew-genlink/contents/Formula/genlink.rb: 403 Resource not accessible by integration []
Error: The process '/opt/hostedtoolcache/goreleaser-action/0.155.0/x64/goreleaser' failed with exit code 1

この Actions では、後に説明する .goreleaser.yml で作成した Homebrew 用の formula ファイルを、最初に作成した homebre-genlink リポジトリに Push します。その際に権限がないと怒られています。

この対処法として、 GitHub の Settings > Developer settings > Personal access tokens から新たに token を作成し、 genlink リポジトリの Settings > Secrets から名前をつけて追加します。このときの名前として GITHUB_TOKEN は使用できないため ACCESS_TOKEN として追加しています。必要 scope は、とりあえず repo にチェックが入っていれば OK です。


.goreleaser.yml は、ビルドのオプションや formula の配置場所などを指定しています。内容は下記の通り。

project_name: genlink
env:
  - GO111MODULE=on
before:
  hooks:
    - go mod tidy
builds:
  - main: .
    binary: genlink
    ldflags: -s -w -X main.version={{.Version}} -X main.revision={{.ShortCommit}} -X main.date={{.Date}}
archives:
  - replacements:
      darwin: darwin
      linux: linux
      windows: windows
      amd64: x86_64
    files:
      - LICENSE
      - CREDITS
release:
  prerelease: auto
brews:
  - tap:
      owner: michimani
      name: homebrew-genlink
    folder: Formula
    homepage: 'https://github.com/michimani/genlink'
    description: 'Generates the URL link in various formats'
    license: "MIT"

内容について少し解説。

builds:
  - main: .
    binary: genlink
    ldflags: -s -w -X main.version={{.Version}} -X main.revision={{.ShortCommit}} -X main.date={{.Date}}

この部分で Go のバイナリをビルドしていますが、その際に ldflags を指定して main パッケージの変数 versionrevisiondate に値を埋め込んでいます。ツールの実装の部分で触れましたが、宣言だけしていた変数はここで値を埋め込むために宣言していたわけです。

こうすることで、ツール内でバージョンやリビジョン情報を表示する際にもソースコードの変更は必要なく、ビルド時に値を埋め込むことができます。

brews:
  - tap:
      owner: michimani
      name: homebrew-genlink
    folder: Formula
    homepage: 'https://github.com/michimani/genlink'
    description: 'Generates the URL link in various formats'

この部分で、作成した formula を配置するリポジトリとディレクトリを指定しています。実際に配置されるのは .rb ファイルで、中身は下記のような内容です。

# typed: false
# frozen_string_literal: true

# This file was generated by GoReleaser. DO NOT EDIT.
class Genlink < Formula
  desc "Generates the URL link in various formats"
  homepage "https://github.com/michimani/genlink"
  version "0.1.3"
  license "MIT"
  bottle :unneeded

  if OS.mac?
    url "https://github.com/michimani/genlink/releases/download/v0.1.3/genlink_0.1.3_darwin_x86_64.tar.gz"
    sha256 "9a4e7ec00e526565578f8b39a5f1d7253bb91b26277d6064b57ff3241d767b75"
  end
  if OS.linux? && Hardware::CPU.intel?
    url "https://github.com/michimani/genlink/releases/download/v0.1.3/genlink_0.1.3_linux_x86_64.tar.gz"
    sha256 "c7ed041d314c16906711bc536ab82a8b170d9c83b7f057bfcfbe1926d9c75d7b"
  end
  if OS.linux? && Hardware::CPU.arm? && Hardware::CPU.is_64_bit?
    url "https://github.com/michimani/genlink/releases/download/v0.1.3/genlink_0.1.3_linux_arm64.tar.gz"
    sha256 "6afa8842542b8ee55833b5af5b9b3b7542b102274c1fe9bfd31408451555dd1f"
  end

  def install
    bin.install "genlink"
  end
end

.goreleaser.yml について、その他のオプションについては公式ドキュメントまたは goreleaser のリポジトリを参照してください。

2-4. タグ付けして Push

あとは、ローカルから Push する際に tag をつけて、 tag も一緒に Push します。

$ git tag v0.1.0
$ git push origin main:main --tag

念のため Actions が起動して正常終了することを確かめておきます。

3. brew コマンドでインストール

Actions が正常に動けば、あとは brew コマンドでインストールするだけです。Homebrew の公式リポジトリに追加されているツールであれば brew install <ツール名> でインストールできますが、 brew tap を使用した配布の場合は下記のようなコマンドでインストールします。

$ brew install michimani/genlink/genlink
...
...
🍺  /usr/local/Cellar/genlink/0.1.3: 4 files, 5.7MB, built in 3 seconds

$ genlink -h

                  _ _       _
  __ _  ___ _ __ | (_)_ __ | | __
 / _' |/ _ \ '_ \| | | '_ \| |/ /
| (_| |  __/ | | | | | | | |   <
 \__, |\___|_| |_|_|_|_| |_|_|\_\
 |___/   Version: 0.1.3-53fc163

Usage:
  genlink [flags] [values]
Flags:
	-u (required)  URL
	-t             Type of link to output
	    md:        Markdown (default)
	    html:      HTML a tag
	    html-bl:   HTML a tag with 'target="_blank"'
	    qr:        QR code image
	-o             Absolute path to directory to output QR code
	               Use this flag in combination with '-t qr'
	               Default is current directory

Author:
  michimani <michimani210@gmail.com>

ツール名を指定する部分が異なるだけで、アップデートやアンインストールについても同様です。

# アップデート
$ brew upgrade michimani/genlink/genlink

# アンインストール
$ brew uninstall michimani/genlink/genlink

まとめ

Go でちょっとした CLI ツールを作ったので、 GitHub Actions と GoReleaser を使って brew コマンドでインストールできるようにしてみた話でした。
配置する YAML ファイルについてはほぼコピペで使えるので、結構簡単に公開できます。

参考にさせていただいたブログに

みなさんも是非こんなものがあったらいいなという CLI ツールを同様の手順で作成、公開して、 GitHub Star 5000兆個を目指してください。

と書かれていたのでこのツールで Star 5000兆個目指したいと思います。

michimani/genlink - GitHub

参考にした記事

下記の記事を参考にさせていただきました。ありがとうございます。

以上、よっしー (michimani) でした。