はじめに

以下の記事で Go で自作したアクセスログパーサーについて紹介しました。

今回はこのモジュールを題材に Go の CI 手法やリリース手法についてざっくり紹介します。以下のような内容です。

  • 静的解析
  • 脆弱性チェック
  • テスト
  • CI
  • バージョン管理
  • リリース

静的解析

Go の数ある Linter/Formatter をひとつに統合したツールである golangci-lint を使います。ツールを個別にインストールする必要がなく、また複数のツールを非同期で実行するため高速です。

インストール

go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

設定

設定は golangci.yaml に書きます。TOML や JSON でも動作するようです。設定ファイルなしでもデフォルト設定で動作しますが、より厳密に解析したいので以下のように設定してみました。重複コードを検出する dupl と長いコードを検出する lll はテストコードでは無効にしています。

run:
  timeout: 5m
linters:
  enable:
    - dogsled
    - dupl
    - errorlint
    - gocritic
    - gocyclo
    - gofmt
    - gofumpt
    - gosec
    - lll
    - makezero
    - misspell
    - nakedret
    - predeclared
    - revive
    - stylecheck
    - tagliatelle
    - thelper
    - tparallel
    - unconvert
    - unparam
    - wastedassign
    - whitespace
issues:
  exclude-rules:
    - linters:
        - dupl
        - lll
      path: _test\.go

使い方

golangci-lint run ./... -v

脆弱性チェック

脆弱性チェックには Go 公式の govulncheck を使います。コマンドラインで Go のソースコードおよびバイナリの脆弱性をチェックできます。

インストール

go install golang.org/x/vuln/cmd/govulncheck@latest

使い方

govulncheck -test ./...

テスト

Go のテストはこんな感じで書いています。使っているデータは S3 の公式ドキュメントで公開されているサンプルをさらにランダム化したものです。

以下のコマンドでユニットテストを実行します。

go test ./... -v -cover -coverprofile=cover.out
  • -cover: コードカバレッジの計測
  • -coverprofile=cover.out: コードカバレッジの結果を cover.out ファイルに出力

CI

これらの静的解析、脆弱性チェック、テストを CI で走らせます。GitHub Actions を使うので以下のような YAML を書きます。上で紹介したツールもほぼ公式の Action があるようです。

name: CI
on:
  push:
    branches:
      - main
      - master
    paths-ignore:
      - "README.md"
  pull_request:
    branches:
      - main
      - master
    paths-ignore:
      - "README.md"

permissions:
  contents: read

jobs:
  test:
    strategy:
      fail-fast: true
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        go: ["1.20", "1.21"]
    runs-on: ${{ matrix.os }}
    steps:
      - name: Set git to use LF
        if: "matrix.os == 'windows-latest'"
        run: |
          git config --global core.autocrlf false
          git config --global core.eol lf

      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup go ${{ matrix.go }}
        uses: actions/setup-go@v4
        with:
          go-version: ${{ matrix.go }}
          cache: false

      - name: Run golangci-lint
        uses: golangci/golangci-lint-action@v3
        with:
          version: v1.54
          args: --timeout=5m

      - name: Run govulncheck
        uses: golang/govulncheck-action@v1
        with:
          go-version-input: ${{ matrix.go }}
          go-package: ./...
          cache: false

      - name: Run tests
        run: |
          git diff --cached --exit-code
          go test ./... -v -cover -coverprofile=cover.out

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: ./cover.out

やっていることは以下の通りです。

  • main または master ブランチに対するプッシュ、プルリクエストを検知して起動
  • ただし変更が README だけの場合は起動しない
  • OS が macos, linux, windows かつ Go のバージョンが 1.20, 1.21 の場合の計 6 パターンをテスト
  • Windows の場合は git での改行コードの扱いを LF に変更
  • Go セットアップ、静的解析、脆弱性チェック、テストを順次実行
  • テストカバレッジを Codecov にアップロード

バージョン管理

gobump はソースに埋め込まれたバージョン情報を検出し、セマンティックバージョニングに従ってインクリメントしてくれます。git と併用することで効果を発揮します。

$ gobump up -w .
? Bump up parser.go:
  ▸ patch (0.0.6 -> 0.0.7)
    minor (0.0.6 -> 0.1.0)
    major (0.0.6 -> 1.0.0)

リリース

ローカルでのタスクランナーとして Makefile を使い、ツールのインストールやテストコマンドを簡易に実行できるようにします。重要な点として、リリースも make コマンドで実行します。一方で GitHub Actions では Windows も扱うので、YAML に make コマンドを書くことは避けています。

GOBIN ?= $(shell go env GOPATH)/bin
VERSION := $$(make -s show-version)

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: 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 --tests

.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 tag "v$(VERSION)"
    git push origin main
    git push origin "refs/tags/v$(VERSION)"

.PHONY: clean
clean:
    go clean
    rm -f cover.out cover.html

リリースは make publish で行います。以下では gobump バイナリの存在を確認し、なければインストールします。

HAS_GOBUMP := $(shell command -v $(GOBIN)/gobump 2> /dev/null)
BIN_GOBUMP := github.com/x-motemen/gobump/cmd/gobump@latest

.PHONY: deps-gobump
deps-gobump:
ifndef HAS_GOBUMP
    go install $(BIN_GOBUMP)
endif

gobump で現在のバージョンを取得し、変数に入れます。

VERSION := $$(make -s show-version)

.PHONY: show-version
show-version: deps-gobump
    $(GOBIN)/gobump show -r .

git リポジトリの状態を確認します。

.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

そして gobump でバージョンを上げ、タグを設定し、push します。

.PHONY: publish
publish: deps-gobump check-git
    $(GOBIN)/gobump up -w .
    git commit -am "bump up version to $(VERSION)"
    git tag "v$(VERSION)"
    git push origin main
    git push origin "refs/tags/v$(VERSION)"

ここまでで、make publish を叩いてバージョンをインクリメントすると自動的にリリースが走るようになりました。

おわりに

かなりざっくりですが、Go モジュールの CI 手法、リリース手法について紹介しました。自分自身まだ個々のトピックを深掘りできていないので、これから手を動かしながら理解を深めていければと思います。