【GitHub Actions】Go言語の自動テストからリリースまでを作ってみた

## はじめに

今回作成したのはGo言語リポジトリ用ワークフロー。
GitHubに指定されたタグがPushされた時点でテストを実行し問題が無ければ、クロスコンパイルを実施しリリースまで行う。
以下にアップロードしているので利用したい方はこちらからどうぞ。

- [github-actions-workflows/release_go.yml at master · x-color/github-actions-workflows · GitHub](https://github.com/x-color/github-actions-workflows/blob/master/workflows/release_go.yml)


ワークフローの流れは以下となっている。

1. タグがPushされたことをトリガーにワークフローが起動
1. 依存したモジュールをダウンロード
1. テストを実施
1. テストに合格した場合、リリースプロセスが始動
1. ソースコードをzipにまとめリリース
1. クロスコンパイルを実施し、それぞれ生成物をzipにまとめてリリース

以下が実際のworkflowファイル

```yml
name: Release Go Project
on:
  push:
    tags:
      - v*

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
    - name: Set up Go 1.13
      uses: actions/setup-go@v1
      with:
        go-version: 1.13

    - name: Check out code into the Go module directory
      uses: actions/checkout@v1

    - name: Get dependencies
      run: go get -v -t -d ./...

    - name: Test code
      run: go test -v .

  setup-release:
    name: Setup release
    needs: test # This workflow is executed after completed 'test' job
    runs-on: ubuntu-latest
    steps:
    - name: Create release
      id: create_release
      uses: actions/create-release@v1.0.0
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        tag_name: ${{ github.ref }}
        release_name: Release ${{ github.ref }}
        draft: false
        prerelease: false

    - name: Get url to upload to release from output
      env:
        url: ${{ steps.create_release.outputs.upload_url }}
      run: |
        mkdir artifact
        echo $url > artifact/url.txt
    - name: Upload artifact to share url with other jobs
      uses: actions/upload-artifact@v1
      with:
        name: artifact
        path: artifact/url.txt

  release-code:
    name: Release Source Code
    needs: setup-release
    runs-on: ubuntu-latest
    steps:
    - name: Download artifact to get url to upload to release
      uses: actions/download-artifact@v1
      with:
        name: artifact

    - name: Get url to upload to release from artifact
      id: get_url
      run: |
        url=$(cat artifact/url.txt)
        echo "##[set-output name=upload_url;]$url"
    - name: Check out code into the Go module directory
      uses: actions/checkout@v1

    - name: Pack source code in zip file
      run: |
        mkdir dist
        ls -la
        cp *.go dist/
        zip -j -r release dist
    - name: Upload release asset
      uses: actions/upload-release-asset@v1.0.1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ steps.get_url.outputs.upload_url }}
        asset_path: release.zip
        asset_name: source.zip
        asset_content_type: application/zip

  release-pkg:
    name: Release package
    needs: setup-release
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [mac64, lin64, win64]
        include:
        - os: mac64
          goos: darwin
          arch: amd64
        - os: lin64
          goos: linux
          arch: amd64
        - os: win64
          goos: windows
          arch: amd64

    steps:
    - name: Set up Go 1.13
      uses: actions/setup-go@v1
      with:
        go-version: 1.13

    - name: Check out code into the Go module directory
      uses: actions/checkout@v1

    - name: Get dependencies
      run: go get -v -t -d ./...

    - name: Build
      env:
        goos: ${{ matrix.goos }}
        goarch: ${{ matrix.arch }}
      run: |
        mkdir dist
        cp test.txt dist/
        GOOS=$goos GOARCH=$goarch go build -v -o dist/app .
        zip -j -r release dist
    - name: Download artifact to get url to upload to release
      uses: actions/download-artifact@v1
      with:
        name: artifact

    - name: Get url to upload to release from artifact
      id: get_url
      run: |
        url=$(cat artifact/url.txt)
        echo "##[set-output name=upload_url;]$url"
    - name: Upload release asset
      uses: actions/upload-release-asset@v1.0.1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ steps.get_url.outputs.upload_url }}
        asset_path: release.zip
        asset_name: release-${{ matrix.os }}.zip
        asset_content_type: application/zip
```

## 各種処理の概要

各種処理は、ジョブとステップの二通りで定義することができる。
今回は、テストや各種リリースの処理はそれぞれジョブとして分離し定義する形で設計している。
これは、クロスコンパイルを実施する際にGitHub Actionsの機能(`matrix`)を有効活用して実施したいと考えたためである。
以降、各処理の概要を説明していく。

### ワークフローのトリガー

以下は、このワークフローが実行される条件を指定している。
この場合は、「v」から始まるタグがpushされた場合にトリガーされ実行されることが定義されている。

```yml
on:
  push:
    tags:
      - v*
```


実行条件は、ほかにも特定のブランチにpushされたや、特定のファイルやディレクトリ下に変更があった場合などを指定することができる。
もちろんpushのみではなく、forkされた場合やissueが立った際なども可能。詳しくは以下のリンクに載っている。

- ブランチやタグによるトリガー条件に付いて：[GitHub Actionsのワークフロー構文 - GitHub ヘルプ](https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#onpushpull_requestbranchestags)
- トリガーイベントについて：[ワークフローをトリガーするイベント - GitHub ヘルプ](https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows)


### 自動テストの実行

以下は、自動テストを実行するジョブが定義されている。

```yml
jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
    - name: Set up Go 1.13
      uses: actions/setup-go@v1
      with:
        go-version: 1.13

    - name: Check out code into the Go module directory
      uses: actions/checkout@v1

    - name: Get dependencies
      run: go get -v -t -d ./...

    - name: Test code
      run: go test -v .
```

各stepにそれぞれ処理が定義されており、上から順に以下の流れとなっている。

1. Go1.13の環境をコンテナ内に作成
2. リポジトリの中に入り、状態をトリガーされたコミットにする
3. `go get`を用いて`go.mod`に記載されている依存関係のあるモジュールをインストール
4. `go test`を用いて、テストを実行

この中で最も重要なのが`actions/checkout@v1`。

- [GitHub - actions/checkout](https://github.com/actions/checkout)

これを実行することにより、リポジトリの中に入ることができる。
そのため、実行しないで`go get` しても必要なモジュールをインストールしてくれない。

もし途中のステップで実行に失敗した場合、デフォルトだとそこで処理は止まり、ジョブは失敗扱いとなる。
例として、`go get`に失敗した場合、このテスト用ジョブはそこで中断され`go test`が実行されることはない。


### リリースの作成

以下は、リリースの作成を定義している。
実際の作成は、`actions/create-release@v1.0.0`に行ってもらっている。

- [GitHub - actions/create-release: An Action to create releases via the GitHub Release API](https://github.com/actions/create-release)

```yml
  setup-release:
    name: Setup release
    needs: test # 'test' ジョブが成功した後に実行する 
    runs-on: ubuntu-latest
    steps:
    - name: Create release
      id: create_release
      uses: actions/create-release@v1.0.0
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        tag_name: ${{ github.ref }}
        release_name: Release ${{ github.ref }}
        draft: false
        prerelease: false
        
    - name: Get url to upload to release from output
      env:
        url: ${{ steps.create_release.outputs.upload_url }}
      run: |
        mkdir artifact
        echo $url > artifact/url.txt
    - name: Upload artifact to share url with other jobs
      uses: actions/upload-artifact@v1
      with:
        name: artifact
        path: artifact/url.txt
```

ジョブは基本的にすべて並列に実行される。
しかし、バグなどの致命的欠陥を持ったままリリースはしたくないため、リリース処理などはテストの実施後に行いたいものである。
そのために、このジョブでは`needs: test`で`test`ジョブが実行完了しないと実行されないように制限を入れている。

これにより、ジョブの逐次実行が可能となる。ジョブに依存関係を持たせたい場合は`needs`を用いるべき。もちろん依存関係は一つのみでなく複数のジョブを指定することができる。詳しくは、以下のリンク参照。

- ジョブの依存関係について：[GitHub Actionsのワークフロー構文 - GitHub ヘルプ](https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idneeds)

このジョブの後半のステップは、作成されたリリースにパッケージをアップロードする際に必要となるURLを次のジョブに渡すために必要となる。
やっていることは`${{ steps.create_release.outputs.upload_url }}`で直前のリリース作成時に出力されたアップロード先URLを取得し、それをファイルに書き込み、`actions/upload-artifact@v1`でワークフローのアーティファクトにアップロードしている。
これだけではジョブ間の情報共有はできないのだが、共有先のジョブで`actions/download-artifact@v1`を実行することでアーティファクト経由で情報を共有することができる。ジョブ間の情報共有については以下のリンクを参照。

- ジョブ間での情報共有について：[Persisting workflow data using artifacts - GitHub ヘルプ](https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts#passing-data-between-jobs-in-a-workflow)

### パッケージのアップロード

以下は、先ほど作成されたリリースにパッケージをアップロードする処理が定義されている。

```yml
  release-pkg:
    name: Release package
    needs: setup-release
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [mac64, lin64, win64]
        include:
        - os: mac64
          goos: darwin
          arch: amd64
        - os: lin64
          goos: linux
          arch: amd64
        - os: win64
          goos: windows
          arch: amd64

    steps:
    #
    # 'test' ジョブと処理がほぼ一緒なので一部割愛
    #
    - name: Build
      env:
        goos: ${{ matrix.goos }}
        goarch: ${{ matrix.arch }}
      run: |
        mkdir dist
        cp test.txt dist/
        GOOS=$goos GOARCH=$goarch go build -v -o dist/app .
        zip -j -r release dist
    - name: Download artifact to get url to upload to release
      uses: actions/download-artifact@v1
      with:
        name: artifact

    - name: Get url to upload to release from artifact
      id: get_url
      run: |
        url=$(cat artifact/url.txt)
        echo "##[set-output name=upload_url;]$url"
    - name: Upload release asset
      uses: actions/upload-release-asset@v1.0.1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ steps.get_url.outputs.upload_url }}
        asset_path: release.zip
        asset_name: release-${{ matrix.os }}.zip
        asset_content_type: application/zip
```

このジョブは実行の定義自体は一種類しかないが、`matrix`を用いることによりパラメータを変化させて同時に3つのジョブを並列に実行するように定義されている。
`matrix`には対象OSごとの実行ファイルを作るために必要なパラメータを設定している。これによりジョブは、ビルド時に必要な情報を取得し、指定したOS用の実行ファイルを並列に生成かつリリースにアップロードすることができる。この仕組みを用いて、クロスコンパイルを実施している。

リリースへのアップロードには`actions/upload-release-asset@v1.0.1`を用いている。これはアップロード先のURLが必要となるので、新規にリリースをアップロードしたい場合は先ほど利用した`actions/create-release@v1.0.0`と併用する必要がある。詳しくは以下のリンクを参照。

- [Releases · actions/upload-release-asset · GitHub](https://github.com/actions/upload-release-asset/releases)

## おわりに

GitHub Actionsが正式リリースされたとのことでいろいろ触ってみたが、結構いろいろ自動化できそうで楽しい。
今回は、触っていた際にたまった知見を活かして、Go言語のリリース用ワークフローを作成してみたが、まだやりたいことをシンプルに実現するにはあと一歩って感じがした。
実現できなくはないが、ちょっとめんどくさいところがまだある（特にジョブ間とワークフロー間の情報共有）ので、今後さらにアップデートを重ねてよりよくなっていくことに期待。