はじめに
以下の記事で Go で自作した CLI ツールについて紹介しました。
今回はこのツールを題材に Go で作成した CLI ツールの CI やリリースについてざっくり紹介します。なお本記事は、Go モジュールの CI について書いたこの記事の CLI ツール版になります。静的解析、脆弱性チェック、テストについてはリンク元とほぼほぼ同じなので割愛しています。
Makefile
CI やリリースに絡んでくるので、先にタスクランナーとして使っている Makefile について紹介します。
BIN := alpen ifeq ($(OS),Windows_NT) BIN := $(BIN).exe endif GOBIN ?= $(shell go env GOPATH)/bin VERSION := $$(make -s show-version) REVISION := $(shell git rev-parse --short HEAD) LDFLAGS := "-s -w -X main.Version=$(VERSION) -X main.Revision=$(REVISION)" HAS_LINT := $(shell command -v $(GOBIN)/golangci-lint 2> /dev/null) HAS_VULNCHECK := $(shell command -v $(GOBIN)/govulncheck 2> /dev/null) HAS_GOBUMP := $(shell command -v $(GOBIN)/gobump 2> /dev/null) BIN_LINT := github.com/golangci/golangci-lint/cmd/golangci-lint@latest BIN_GOVULNCHECK := golang.org/x/vuln/cmd/govulncheck@latest BIN_GOBUMP := github.com/x-motemen/gobump/cmd/gobump@latest export GO111MODULE=on .PHONY: build build: clean go mod tidy go build -ldflags "-X main.Version=$(VERSION) -X main.Revision=$(REVISION)" -o $(BIN) . .PHONY: put put: build cp $(BIN) $(GOBIN)/$(BIN) .PHONY: check check: test cover golangci-lint govulncheck .PHONY: deps deps: deps-lint deps-govulncheck deps-gobump .PHONY: deps-lint deps-lint: ifndef HAS_LINT go install $(BIN_LINT) endif .PHONY: deps-govulncheck deps-govulncheck: ifndef HAS_VULNCHECK go install $(BIN_GOVULNCHECK) endif .PHONY: deps-gobump deps-gobump: ifndef HAS_GOBUMP go install $(BIN_GOBUMP) endif .PHONY: test test: go test ./... -v -cover -coverprofile=cover.out .PHONY: cover cover: go tool cover -html=cover.out -o cover.html .PHONY: golangci-lint golangci-lint: deps-lint golangci-lint run ./... -v .PHONY: govulncheck govulncheck: deps-govulncheck $(GOBIN)/govulncheck -test ./... .PHONY: show-version show-version: deps-gobump $(GOBIN)/gobump show -r . .PHONY: check-git ifneq ($(shell git status --porcelain),) $(error git workspace is dirty) endif ifneq ($(shell git rev-parse --abbrev-ref HEAD),main) $(error current branch is not main) endif .PHONY: publish publish: deps-gobump check-git $(GOBIN)/gobump up -w . git commit -am "bump up version to $(VERSION)" git push origin main .PHONY: release release: check-git git tag "v$(VERSION)" git push origin "refs/tags/v$(VERSION)" .PHONY: clean clean: go clean rm -f $(BIN) cover.out cover.html
使うツールややることはモジュールの場合と大差ないのですが、以下が違います。
- ビルドも定義している
- ビルド時はバージョンとリビジョンを指定する
- リリース直前のプッシュとリリースを分離している (それぞれでワークフローが走るため)
リリース直前の commit/push は make publish
で行います。モジュールの時はこのコマンドでタグをプッシュしてリリースまでやっていましたが、今回はバイナリを作ってリリースページに置く必要があるので分離しています。
.PHONY: publish publish: deps-gobump check-git $(GOBIN)/gobump up -w . git commit -am "bump up version to $(VERSION)" git push origin main
make publish
すると GitHub Actions で静的解析、脆弱性チェック、テストが走ります。これを見届けて結果が OK であればリリース可能な状態とみなします。make release
すると今度はタグを作ってプッシュし、リリースのためのワークフローが起動します。
.PHONY: release release: check-git git tag "v$(VERSION)" git push origin "refs/tags/v$(VERSION)"
CI
繰り返しになりますが、GitHub Actions のワークフローは以下 2 つを用意しています。
- ci.yml: 静的解析、脆弱性チェック、テスト
- release.yml: リリース
├── .github │ └── workflows │ ├── ci.yml : └── release.yml
ci.yml は以下のような設定です。
- main または master ブランチに対するプッシュ、プルリクエストを検知して起動
- ただし変更が README だけの場合は起動しない
- タグをプッシュする場合も起動しない (別のワークフローを起動するため)
- Go セットアップ、ビルド、静的解析、脆弱性チェック、テストを順次実行 (ここでのビルドは確認の意味)
- モジュールではなくアプリであり最終的にバイナリを配布するので、Go のバージョンは最新のみを対象とする
- OS も ubuntu のみを対象としている
- ubuntu のみなので、ビルドやテストでは
make
コマンドをそのまま書いている
name: CI on: push: branches: - main - master paths-ignore: - "README.md" tags-ignore: - "v[0-9]+.[0-9]+.[0-9]+" pull_request: branches: - main - master paths-ignore: - "README.md" permissions: contents: read jobs: test: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Setup go uses: actions/setup-go@v4 with: go-version: 1.21.4 cache: false - name: Unshallow run: git fetch --prune --unshallow --tags - name: Build run: make build - name: Run golangci-lint uses: golangci/golangci-lint-action@v3 with: version: v1.54 args: --timeout=10m # https://github.com/golangci/golangci-lint-action/issues/244 skip-pkg-cache: true skip-build-cache: true - name: Run govulncheck uses: golang/govulncheck-action@v1 with: go-version-input: 1.21 go-package: ./... cache: false - name: Run tests run: | git diff --cached --exit-code make test make cover - name: Archive code coverage results uses: actions/upload-artifact@v3 with: name: code-coverage-report path: cover.html
リリース
リリースのワークフローは以下のように非常に単純です。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@v3 with: fetch-depth: 0 - name: Setup go uses: actions/setup-go@v4 with: go-version: 1.21.4 - name: Run goreleaser uses: goreleaser/goreleaser-action@v4 with: version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GoReleaser に設定を読ませるための YAML が別途必要です。リポジトリのルートに .goreleaser.yml
を配置します。
project_name: alpen env: - GO111MODULE=on before: hooks: - go mod tidy builds: - main: ./ binary: alpen ldflags: - -s -w - -X github.com/nekrassov01/alpen/main.Version={{.Version}} - -X github.com/nekrassov01/alpen/main.Revision={{.ShortCommit}} env: - CGO_ENABLED=0 goos: - linux - windows - darwin archives: - format: tar.gz name_template: >- {{ .ProjectName }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} format_overrides: - goos: windows format: zip checksum: name_template: "checksums.txt" snapshot: name_template: "{{ .Version }}-devel" changelog: sort: asc filters: exclude: - "^docs:" - "^test:"
GoReleaser は CLI ツールなので、インストールして goreleaser init
すればテンプレートが吐かれます。これを少し手直しするだけで十分だと思います。ここまでで、make release
を叩いてバージョンをインクリメントすると自動的に各 OS 向けのバイナリ作成、GitHub へのリリースまでが行われるようになりました。
注意点
ここまでやっていざリリースをした際、ワークフローが権限不足で失敗したことがありました。GitHub リポジトリで以下の設定変更が必要でした。
- Settings > Actions > General > Workflow permissions を Read and write permissions に変更
おわりに
かなりざっくりですが、Go 製 CLI アプリの CI およびリリースについて紹介しました。周辺ツールが充実していて簡単に CI 環境を構築できるので、さほど難しさは感じませんでした。