はじめに
以下の記事で 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 環境を構築できるので、さほど難しさは感じませんでした。