Go製アプリケーションのリリース自動化
Goで書いたアプリケーションをリリースする際にやらないといけないことはいろいろある。
まず当然のこととしてコンパイルして成果物を作り、それをリリースする。
せっかくクロスビルドが容易なGoを使うので配布する成果物も可能な限り幅広い環境に対応させたい。
また、リリースに含まれる変更の概要をまとめたいわゆるChangelogのようなものも簡単に作れるとなおよい。
GitHubでホストするリポジトリのリリースについて主に考えたいので、リリースのホスティングはGitHubのReleasesになる。
各Releaseの説明をChangelogとし、変更がもたらされたPull Requestのリンクも添えたい。
リリースするということは新しいバージョンを決めて、Gitのタグを打つ必要もある。
ひとつひとつはよくある作業だし特段難しいことはないが、いちいち手でやっていられないのでCIに任せたい。
掲題の通りGitHub Actionsでこれら一連の作業を自動化させるという話。
GitHub Actionsによるリリース自動化それ自体は目新しいトピックではない。しかしこの記事で紹介するワークフローはgit pushする以外に開発者がとるべきアクションはない。
具体的には、世の中の先行事例はGitのタグ発行は開発者の手作業で、それを契機にパイプラインを開始する……というものばかりだがそれも不要になる。
goreleaserにビルドまわりを任せる
最近はgoreleaserというツールがよく使われているのでビルドまわりはこれに任せる。
goreleaserには:
- アプリケーションをビルドする
- ビルドした成果物をGitHub Releasesにアップロードする
……という2点を任せる。
アップロード対象のリリースは実行したリポジトリに存在する最新のタグから同定される。
言い換えるとタグを打つのはgoreleaserの責務外となる。
semantic-releaseにバージョニングを任せる
[semantic-releaser][]というツールがある。JavaScript (Node) で実装されたCLIツールでConventional Commitsに従ってコミットログを書いておくとよしなに次のリリースバージョンを決めてくれる。
Conventional CommitsはSemantic Versioningを参照し、たとえば feat: blah blah ...
とか書くと新機能の追加なのでminorの更新を含意する、などのルールを定義している。
Conventional Commitsに従うとコミットログから次のバージョンを決めるルールがツールを越えて相互運用できる。
他にもプラグインでGitHub Releasesを作ったりGitタグを打ったり、あるいはNPMにアップロードするなどができる。
JavaScriptで書かれていることからもわかるように元々NPMパッケージのリリースを主眼に置いてエコシステムが整えられたツールだが、前述のようにリリースバージョンの決定以外はプラグインとして実装されているので、NPMパッケージのリリース以外にも使える。
必要なのはConventional Commitsに従ってコミットすることだけ。
つまり semantic-releaseを使ってGitタグの発行を含むバージョンアップ作業を自動化し、発行された新しいタグに成果物を作成・紐付ける作業をgoreleaserで自動化する というのがワークフローのあらましになる。
ワークフロー例
実際のワークフローを例に説明する。
pkgboundaries/ci.yml at main · aereal/pkgboundaries
上記ファイルから抜粋・簡略化したYAMLが以下。
determine_release:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
outputs:
will_release: ${{ steps.determine_release.outputs.new_release_published }}
steps:
- uses: actions/checkout@v3
- id: determine_release
uses: cycjimmy/semantic-release-action@v3.0.0
with:
dry_run: true
env:
GITHUB_TOKEN: ${{ github.token }}
release:
runs-on: ubuntu-latest
needs:
- determine_release
if: ${{ needs.determine_release.outputs.will_release }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: cycjimmy/semantic-release-action@v3.0.0
env:
GITHUB_TOKEN: ${{ github.token }}
- uses: actions/setup-go@v3
with:
go-version: '1.18.x'
- uses: actions/cache@v3
with:
path: ~/go/pkg/mod
key: go-${{ hashFiles('**/go.sum') }}
restore-keys: |
go-
- uses: goreleaser/goreleaser-action@v2.9.1
with:
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ github.token }}
determine_releaseとreleaseという2つのジョブに分けている。
determine_releaseは次のリリース予定を調べるジョブで「実行時点で新しいバージョンが発行されそうか」を示す真偽値 will_release
を出力する。
cycjimmy/semantic-release-actionというsemantic-releaseを実行するActionがあり、その outputs.new_release_published
という真偽値を参照している。
determine_releaseでは dry_run: true
を指定し実際のリリースは行わず、バージョンアップ予定だけを調べる。
releaseジョブは実際にリリースを行う。
まず needs
にdetermine_releaseを指定する。これは単に依存関係を宣言することに加えて needs
コンテキスト経由でdetermine_releaseジョブの出力にアクセスする目的もある。
if: ${{ needs.determine_release.outputs.will_release }}
で「 needs.determine_release.outputs.will_release
がtrueだったらreleaseジョブを実行する」という意味になる。
新しいバージョンアップを要する変更がコミットされていなければリリース作業は行われない。
参考:
- Workflow syntax for GitHub Actions - GitHub Docs
- Contexts - GitHub Docs
続いてsemantic-releaseを実行し、GitHub Releasesを作る。
以下にsemantic-releaseの設定を引用する:
{
"branches": [
"main"
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/github"
]
}
from pkgboundaries/.releaserc.json at 8f58c3dd5880534c1fd8cc8f513795722b52b279 · aereal/pkgboundaries
特別な設定は必要としない。デフォルトでNPMパッケージを公開する @semantic-release/npm が有効になっているのでそれ以外の有用なプラグインだけを明示しているだけに留まる。
最後にgoreleaserを実行する。
goreleaser-actionというインストールから実行までをよしなにやってくれるActionがあるのでこれを使う。
goreleaser/goreleaser-action: GitHub Action for GoReleaser
goreleaserはYAMLでいろいろ挙動をカスタマイズできるのだが、今回重要なのは release.mode
になる。
release:
mode: keep-existing
from pkgboundaries/.goreleaser.yml at 8f58c3dd5880534c1fd8cc8f513795722b52b279 · aereal/pkgboundaries
goreleaserもリリースノートの作成やGitHub Releasesの作成ができるが、今回のワークフローではそれらはsemantic-releaseに任せている。
mode: keep-existing
を選ぶとタグに対応するGitHub Releasesが既に存在したらタイトルや本文を置き換えず成果物だけアップロードするという挙動になる。
こうすることでsemantic-releaseとうまく共存できる。
今後
リリース時に人間による確認を挟みたいという場合には、Environmentsが使えるかもしれない。
参考: Using environments for deployment - GitHub Docs
EnvironmentsはDeploymentsに関連する概念で、その名の通りデプロイ対象の環境を表す。
Environmentsはそれぞれデプロイ時に必須となるcommit statusやレビュアーを指定できる。
これら機能はBranch protection rulesと似ていて、たとえばproduction環境では所定のチームやユーザのレビューなしにデプロイできない、といった設定ができる。
参考:
GitHubにおけるDeploymentsは単にイベント履歴とそれらに応じたWebhookでしかない。
最近追加されたGitHub Actionsとの統合では、ジョブに environment: production
のように記述することである環境の利用を宣言できる。
このジョブ実行開始時にDeployments APIでdeploymentとdeployment_statusが作成される。完了するとsuccessもしくはfailureの記録がGitHub Actionsによって行われる。
実際のデプロイ処理はジョブのstepとして自由に定義できるので、この記事で紹介したワークフローをデプロイ処理として記述することもできる。
するとEnvironmentsの機能でDeploymentsの開始時にチーム・ユーザによる確認を挟んだ上でリリースする、といったことが実現できる。
これは規模の大きいチームで厳格な統制を図りたい時に便利かもしれない。