blog.syfm

徒然なるままに考えていることなどを書いていくブログ

Pull Request と共に GoDoc を生成してくれる GitHub Action をつくった

これは Go5 Advent Calendar 2019 の 13 日目の記事です。12 日目は shiimaxx さん、14 日目は nsega さんです。


以前、以下のような記事を書いており、その中で GoDoc を通してソフトウェアを見ることでドキュメントや仕様の質を確認しやすくなるといったメリットを紹介していました。

syfm.hatenablog.com

この記事の最後に

本当は GitHub で Pull Request をつくるときにも変更を元に生成したドキュメントを見れるようにしてレビューできるようにしたいけど、あまり良い方法が思いつかないのでできていない。いずれ何かの仕組みをつくってやっていきたいと思っている。

と書き残していましたが、最近になり GitHub Actions の正式バージョンがリリースされたため、これを利用して Pull Request がつくられたときにそのブランチを元に GoDoc を生成してくれる Action をつくりました。

github.com

github.com

まだ荒削りの状態ですが、基本的な機能は提供できていると思います。以下のように設定することで、Pull Request がつくられたときに Action が動き、GoDoc が生成され、GitHub Pages へデプロイされ、Pull Request のコメントに GoDoc へのリンクが投稿されます。

f:id:ktr_0731:20191213224232p:plain

add Go files by ktr0731 · Pull Request #1 · ktr0731/godoc-action-example · GitHub

実装はシェルスクリプトで素朴に行っています。実際にやっていることは以下のとおりです。

  1. godoc コマンドで GoDoc サーバを起動
  2. スキャン中は 404 が返ってくるので 200 が返ってくるまでポーリング
  3. Wget で対象リポジトリの GoDoc をクローリング
  4. gh-pages へ commit
  5. GoDoc のリンクをコメント通知

現状、GoDoc の HTML 形式をファイルへ生成する手段がないため GoDoc サーバを起動することにしています。もしもっと良いやり方があれば教えて下さい。

注意点としては、GitHub Pages はプライベートリポジトリでも利用することができますが、誰からもアクセス可能な状態になっており認証を設けることもできないという点があります。そのため、プライベートリポジトリでの使用はおすすめできません。

この Action ができることは素朴ですが、案外便利かと思うので良かったら試してみてください。

Go 1.13 にアップデートするとテスト時に "flag provided but not defined" エラーが発生するケース

Go でテストを実行する際は動的に生成された main 関数がエントリポイントとなっています。
Go 1.13 からはテスト実行時のテストフラグ (-v-coverprofile など) の登録がこの生成された main 内で行われるようになったため、 main より先に実行される init 内でフラグの登録を行っているテストではフラグのパースがうまく動かず、テストが落ちるようになります。

たとえば、自分が開発している Evans では golden files testing のために --update フラグを e2e パッケージで用意し、これが有効になっている場合のみ golden files を更新するような設定になっていました。

var (
    update = flag.Bool("update", false, "update goldens")
)

func init() {
    flag.Parse()
    if *update {
        os.RemoveAll(filepath.Join("testdata", "fixtures"))
        if err := os.MkdirAll(filepath.Join("testdata", "fixtures"), 0755); err != nil {
            panic(fmt.Sprintf("failed to create fixture dirs: %s", err))
        }
    }
}
$ go test -v
flag provided but not defined: -test.v
Usage of /var/folders/p8/yjvb_xcn54d3dsfjz76rywvh0000gn/T/go-build261037663/b001/e2e.test:
  -update
        update goldens
exit status 2
FAIL    github.com/ktr0731/evans/e2e    0.018s

これを回避するためには Go 1.13 で新たに追加された testing.Init()init 内で呼びます。
これはテストフラグを明示的に登録してくれる関数で、これをフラグのパース前に呼ぶことで正常にパースすることができるようになります。

var (
    update = flag.Bool("update", false, "update goldens")
)

func init() {
    testing.Init()
    flag.Parse()
    if *update {
        os.RemoveAll(filepath.Join("testdata", "fixtures"))
        if err := os.MkdirAll(filepath.Join("testdata", "fixtures"), 0755); err != nil {
            panic(fmt.Sprintf("failed to create fixture dirs: %s", err))
        }
    }
}

モノレポで Go Modules を使う

Multi-Module Repository

Go Modules では Git で管理されているリポジトリ内に複数のモジュールを定義することが可能です。*1
これを利用すると、たとえばプロジェクト内で使う Go ツールの依存管理をプロジェクトのコードとは異なる go.mod で行うことができます。

$ go mod init # create project go.mod
$ mkdir tools && cd tools
$ go mod init # create tools go.mod
$ cat <<EOF
// +build tools

package tools

import (
        _ "github.com/rakyll/statik"
)
EOF
$ go get -tags tools ./... # add tool dependencies to go.mod

このとき、ディレクトリはこのような構造になっています。

.
├── go.mod
├── go.sum
└── tools
    ├── go.mod
    ├── go.sum
    └── tools.go

モジュール単位のバージョニング

Go modules はモジュール単位でバージョニングを行うため、モジュールを分ければ当然バージョニングも別々にできるようになります。
Go modules では VCS のタグから prefix を解釈することができ、マルチモジュール構成の場合は prefix を付けることでモジュール単位でバージョニングできます。
prefix はリポジトリ内のパスに対応し、ルートモジュールの prefix は空文字列であるため、単純に v0.1.0 といった見慣れたタグ名になります。 上記のマルチモジュール構成の場合、 tools のバージョニングには tools を prefix としてつけ、 tools/v0.1.0 といったタグ名になります。

モノレポと Go Modules

既存のベンダリングツールではリポジトリ単位でしかバージョニングができませんでしたが、Go modules を使うと上記のようにモノレポでも異なるリリースサイクルを持つことができるようになります。

また、Go プロジェクトのモノレポにおいて Renovate *2 のような依存パッケージ・モジュールの更新を行ってくれる bot との相性が悪くなっていました。
たとえば google-cloud-go は、GCP のサービス群のライブラリがすべて置かれているリポジトリですが、従来ではリポジトリ単位でしかバージョニングできなかったため、プロジェクトで使っていないパッケージの更新によりバージョンが上がった場合でも Renovate が動作してしまうといった問題がありました。
しかし、Go modules により必要なモジュールだけを依存対象に含められるようになっため、このような問題は解消することができるようになりました。

google-cloud-go はマルチモジュール構成のお手本としても参考になります。

Releases · googleapis/google-cloud-go · GitHub

今回紹介したことは Go Wiki にも記載されているため、詳しくはこちらを見てみてください。

Modules · golang/go Wiki · GitHub