Goのリリースアセットの配置を自動化したい

# これを自動化したい

GitHubのReleaseにクロスコンパイルで作成したアーカイブを配置（Assets）

こんなかんじ

> ![スクリーンショット 2021-12-30 19.43.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/908316/0002d5a2-dfb0-c2dd-6ea5-36dc424eab2b.png)
> [Prometheus](https://github.com/prometheus/prometheus/releases)

# 方法

GitHub ActionsのパイプラインでOS・CPUアーキテクチャごとのアーカイブを作成し、リリースに配置します。

GitHub Actionsにアーカイブを作成するコマンドを書いてもいいのですが、自分はMakefileで書いてみました。
（GitLabを使うことが多いのでそちらに流用できるように）

- [./Makefile](#Makefile)
- [./.github/workflow/release.yml](#GitHub%20Actions%20workflow)

## Makefile

### ビルドやアーカイブファイルの作成用の処理

```Makefile
GO ?= go
GOOS ?= $(shell $(GO) env GOOS)
GOARCH ?= $(shell $(GO) env GOARCH)
MODULE_NAME ?= $(shell cat go.mod | head -n1 | cut -f 2 -d ' ')

all: build

# パッケージのダウンロード
.PHONY: get
get:
	go mod download


# ビルド (変数にOSやCPUアーキテクチャをいれてクロスコンパイル)
# ビルド後のファイルの配置場所 ： `./.build/<OS>-<CPUアーキテクチャ>/*

.PHONY: build
build: get
	mkdir -p .build/$(GOOS)-$(GOARCH)/
	GOOS=$(GOOS) GOARCH=$(GOARCH) $(GO) build
	if [ $(GOOS) = "windows" ] ; then \
		mv ./$(MODULE_NAME).exe ./.build/$(GOOS)-$(GOARCH)/ ; \
	else \
		mv ./$(MODULE_NAME) ./.build/$(GOOS)-$(GOARCH)/ ; \
	fi ; \


# アーカイブの作成 （必要なファイルを含んだアーカイブファイルをOS・CPUアーキテクチャを指定して作成）
# アーカイブファイルの配置場所 : `./packages/<Gitのタグ>/*

TAG ?= $(shell git tag | tail -n1)
.PHONY: package
package:
	mkdir -p ./packages/$(TAG)/$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH)
    cp -r LICENSE README.md \
        ./packages/$(TAG)/$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH)
	if [ $(GOOS) = "windows" ] ; then \
		cp ./.build/$(GOOS)-$(GOARCH)/$(MODULE_NAME).exe ./packages/$(TAG)/$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH) ; \
	else \
		cp ./.build/$(GOOS)-$(GOARCH)/$(MODULE_NAME) ./packages/$(TAG)/$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH) ; \
	fi
	cd ./packages/$(TAG) ; \
	if [ $(GOOS) = "windows" ] ; then \
		zip -r $(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH).zip ./$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH) ; \
	else \
		tar cvf $(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH).tar.gz ./$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH) ; \
	fi ; \
	rm -r ./$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH)


# 削除用

.PHONY: clean
clean:
	-rm -r ./.build ./packages ./.build.env
```

`make`でビルド、`make package`でアーカイブファイルの作成
`GOOS=linux GOARCH=amd64`などを指定してコンパイルするOS・CPUアーキテクチャの指定ができます。

`package`の処理はアプリケーションごとに必要なファイルが変わってくると思うので各々書き換えてください。
この部分の１行目が必要なファイルを指定している箇所で、２行目に指定しているディレクトリにコピーされます。

```shell
cp -r LICENSE　README.md \
    ./packages/$(TAG)/$(MODULE_NAME)-$(TAG).$(GOOS)-$(GOARCH)
```

### Goが対応しているすべてのOS・CPUアーキテクチャでアーカイブファイルをそれぞれ作成する処理

```makefile
# Goが対応しているOS・CPUアーキテクチャのリストからそれぞれビルドしてアーカイブファイルを作成
# アーカイブファイルの配置場所 : `./packages/<Gitのタグ>/*

.PHONY: package-all-with-build
package-all-with-build: get
	$(GO) tool dist list | grep 'aix\|darwin\|freebsd\|illumos\|linux\|netbsd\|openbsd\|windows' | while read line ; \
	do \
		printf GOOS= > ./.build.env ; \
		echo $$line | cut -f 1 -d "/" >> ./.build.env ; \
		printf GOARCH= >> ./.build.env ; \
		echo $$line | cut -f 2 -d "/" >> ./.build.env ; \
		. ./.build.env ; \
		make build GOOS=$$GOOS GOARCH=$$GOARCH ; \
		make package GOOS=$$GOOS GOARCH=$$GOARCH ; \
	done
	rm ./.build.env
```

`go tool dist list`コマンドでリストを取得し、シェルのwhlieループでそれぞれビルドとアーカイブファイルの作成をしています。
`grep`することで、想定していないOS（例の場合はandroid, ios等）を除外し、特定のOSだけビルドするようにしています。

こんなかんじのを１行ずつ読んでループさせてます。

```shell
$ go tool dist list | grep 'aix\|darwin\|freebsd\|illumos\|linux\|netbsd\|openbsd\|windows'
aix/ppc64
darwin/amd64
darwin/arm64
freebsd/386
freebsd/amd64
freebsd/arm
freebsd/arm64
illumos/amd64
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/riscv64
linux/s390x
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
openbsd/mips64
windows/386
windows/amd64
windows/arm
windows/arm64
```

## GitHub Actions workflow

Makefileが書けたら次はGitHub Actionsのワークフローを書く。

```yaml
name: "<任意の名前>"

# `v*`に一致するタグがPushされたらパイプラインを実行
on:
  push:
    tags:
      - "v*"

jobs:
  # Goのセットアップ
  setup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Set up Go
        uses: actions/setup-go@v2
        with:
          go-version: 1.17

  # ビルド・アーカイブファイルの作成とリリースアセットのアップロード
  release:
    needs: setup
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      # Makefileで書いたアーカイブファイルの作成処理を実行
      - name: Build and package
        run: make package-all-with-build TAG=${GITHUB_REF##*/}

      - uses: actions/upload-artifact@master
        with:
          name: packages
          path: ./packages

      # 作成した`*.tar.gz`と`*.zip`をアップロード
      - run: |
          set -x
          assets=()
          for asset in ./packages/${GITHUB_REF##*/}/*.tar.gz ./packages/${GITHUB_REF##*/}/*.zip ; do
            assets+=("-a" "$asset")
          done
          tag_name="${GITHUB_REF##*/}"
          hub release create "${assets[@]}" -m "$tag_name" "$tag_name"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

これでGitHubでタグを作るとパイプラインが実行されてリリースページにアセットを配置できます。

# あとがき

シェルスクリプトを紹介してばかりになってしまった・・・難しい。
パッケージを作成するときの必要なファイルをコピーする部分だけ各々書き換えて貰えれば使えると思うので参考にしてもらえれば嬉しいです。

## 参考

- [【シェルスクリプト】ファイルの中身を一行ずつ読み込む方法](https://www.server-memo.net/shellscript/read-file.html)
- [Makefile ことはじめ](https://qiita.com/kasei-san/items/ad25df63260e86c5cc71)
- [How to upload multiple assets? #28](https://github.com/actions/upload-release-asset/issues/28#issuecomment-617208601)