様々な環境(OS・CPUアーキテクチャ)向けのインストールパッケージやDockerイメージを提供しなければならない、例えばCLIツールのようなものをGo言語で作るとき、多くの人がGoReleaserを利用しているでしょう。
GoReleaserは動作検証のために手元で動かすこともありつつ、ほとんどのユースケースではGitHub ActionsなどのCIツール上で動かすでしょう。(リリースが手動の場合には手元で動かすのがメインの人もいるかもしれませんね。)
GitHub Actions上でGoReleaserを利用するとき、ドキュメントに従うとgoreleaser/goreleaser-actionというaction経由でGoReleaserをインストール・起動することになります。
一方、Go 1.24でgo.modにtoolディレクティブが追加されたことで、generate.goファイルを利用していたワークアラウンドに代わって、開発に利用するGo製ツールを管理できるようになりました。これでGoReleaserもgoのToolsの管理下に入れて呼び出す選択肢も浮上しました。
このエントリではGitHub Actions上でGoReleaserを利用する2つの手法を比較します。
goreleaser-action経由で使う
ドキュメント通りに以下のようなGitHub Actionsのワークフロー定義を構築するだけです。
jobs: goreleaser: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - uses: actions/setup-go@v5 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: version: '~> v2' args: release --clean
メリットは、公式で案内されている手法で安心感があることです。コンパイル済みのバイナリを持ってくるので、セットアップにかかる時間も比較的高速です。
デメリットは、利用するGoReleaserのバージョンの宣言がGitHub Actions側に寄ることです。CIで利用しているGoReleaserのバージョンと手元のバージョンを仕組みで揃えるのが厳しくなります。
go tool経由で使う
go.modのtoolディレクティブに以下のように記述します。GoReleaserは現在v2なのでメジャーバージョンの指定も必要なことに注意してください。
tool github.com/goreleaser/goreleaser/v2
go toolコマンド経由でGoReleaserを呼び出せます。
$ go tool goreleaser --version ____ ____ _ / ___| ___ | _ \ ___| | ___ __ _ ___ ___ _ __ | | _ / _ \| |_) / _ \ |/ _ \/ _` / __|/ _ \ '__| | |_| | (_) | _ < __/ | __/ (_| \__ \ __/ | \____|\___/|_| \_\___|_|\___|\__,_|___/\___|_| goreleaser: Release engineering, simplified. https://goreleaser.com GitVersion: v2.12.7 GitCommit: unknown GitTreeState: unknown BuildDate: unknown BuiltBy: unknown GoVersion: go1.25.3 Compiler: gc ModuleSum: h1:C2EgMDSQQvFpaZwA4L7JuopZzrvvNuVvdHA52AyCT0M= Platform: darwin/arm64
あとは、CIで実際に利用したいGoReleaserの(go toolを経由した)コマンド呼び出しを、GitHub Actionsのワークフロー定義に入れていきます。
jobs: goreleaser: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - uses: actions/setup-go@v5 - name: Run GoReleaser run: go tool goreleaser release --clean
メリットは、GitHub Actionsでも手元でも同じGoReleaserのバージョンに揃えられることです。
GoReleaserは2024年にメジャーバージョンがv2となり、このタイミングで破壊的な変更が入っています。また、現在も継続的に開発が続けられており、新たな設定が利用可能になったり、元々使えていた設定が非推奨になったりする変化があります。GoReleaserのバージョンをgo.modで宣言・集約的に管理できることには一定の価値があります。
また、GitHub Actionsに依存しないやり方なので、GitLabなど他のCIツールでも同じように利用できるはずです。
デメリットは、CI環境でのGoReleaserインストールが遅いことです。setup-go actionのキャッシュがツールのバイナリにも効くと期待していたのですが、そうではなさそうです。GitHub Actions上で実行したところ、installフェーズに1分30秒ぐらいかかりました。
さらにもう一つ致命的なデメリットがあります。go.modのtoolディレクティブで管理しているツールのgo.modに書かれているgoディレクティブは、自分たちのモジュールのgo.modにも影響を及ぼします。例えば、元々の自分たちのgo.modにgo 1.24と書かれていても、現時点で最新のGoReleaser v2を依存に加えてしまうとgo 1.25.1に勝手に上げられてしまいます。
これは、バイナリを提供しつつライブラリとしての機能も果たすようなプロジェクトで特に困ります。なぜなら、そのモジュールに依存したソースコードをビルド可能なGoのバージョンが引き上げられてしまうからです。Go言語では直近2つのメジャーリリースがサポートされていますが、先ほどの例だと、そのモジュールはサポート下のGo 1.24の環境では利用できなくなるということです。
go.modのセマンティクスとしては、goディレクティブはコンパイル可能な最低限のバージョン、toolchainディレクティブは推奨されるツールチェーンバージョンを表します。しかし、ツール提供だけを目的としたモジュールではこの区別をせず、単にgoディレクティブを最新に近いバージョンに設定していることがあります。GoReleaserもその一つです。setup-go actionでgo-version-fileにgo.modを指定するとgoディレクティブを読んでバージョンを決める機能があることもこの状況を助長していると感じています。
所感
特にこだわりがなければ、公式が案内しているgoreleaser-actionを利用するやり方が無難で良いと思います。
CIが多少遅くてもよく、ビルド可能なGoバージョンの下限が上がっても構わないなら、手元でもCIでも同じGoReleaserを使えるというメリットを取って、go tool経由で利用するのもアリかもしれません。