github actionsでリリースにかかる付帯作業の自動化を行なう

# はじめに

今までチームのリリース作業そのものは github actions を利用して自動化ができていましたが、
それに伴う付帯作業は手作業で行っていました。
例えば、

- リリース時に Slack でのリリース開始報告
- リリースノートの手動管理
- リリースバージョンの手動管理
- リリース完了後の Slack でのリリース完了報告

などです。
これらについて地味に面倒臭い作業だったので、github acitons で全部自動化しちゃおうというのが今回の内容です。

# 概要(完成形)

pipeline の実行ボタンをポチッと押せば**デプロイ〜リリースノート管理〜リリース連絡**まで自動でやってくれるパイプラインです。

![002_run_workflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/6cae42e7-afb5-191a-68f0-11b998d7eeaf.png)


## リリース（正常)

![pipeline_desc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/e2ba7bec-f852-baed-6624-2c0bad3650af.png)



以下の点を満たします。

- リリースバージョンを自動でカウントアップして設定したい
- マイナーリリースバージョンだけでなく、メジャーリリースバージョンも指定したい
- 任意のバージョン指定もできればやりたい
- リリース時/リリース完了時に Slack で連絡したい
- リリース完了時に Slack でリリースした内容を連絡したい
- Slack の開発チーム用のチャンネルにもリリース状況を報告したい
- デプロイ完了後、リリースノートを作成したい
- リリースノートの内容に前回リリース時からの PR 差分内容を記載したい

**リリース事前通知(通知例)**
![003_pre_release_slack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/90f2d1fa-0ae5-789b-886a-c0a225b799b7.png)


**リリース完了通知(通知例)**
![010_done_release_slack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/ee45710b-52b1-bf88-a420-9a493f5e500e.png)


**リリース完了通知(開発チーム向け)**
![005_done_release_dev_slack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/4595b18d-ccdb-7557-4ed0-564d5f195af0.png)


**リリースノート自動生成**
![009_release_note2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/f9ad8df1-8ae6-e629-76fd-d8e842a2f0d9.png)


## リリース（異常発生時)

![pipeline_error_desc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/69846397-64a1-d6ff-ebca-614af19d15d2.png)


以下の点を満たします。

- Slack の開発チーム用のチャンネルにもリリース Failed を報告したい

**リリース異常通知(開発チーム向け)**
![006_err_release_slack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/801247c5-0fc8-646b-f0f3-f32a6e7c8dcf.png)


# actions.yml 全体

カウントアップ周りのロジックが入れ子構造になって大分冗長な感じになっている気がしますが、動きはします。

```yml
name: release input test

on:
  workflow_dispatch:
    inputs:
      major_release:
        description: メジャーバージョンリリース カウントアップ(optional)
        type: boolean
        default: false
        required: false

      release_version:
        description: リリースバージョンの明示的な指定(optional)  入力した場合はmajor_releaseオプションは無視される。
        type: string
        required: false

env:
  SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}

jobs:
  release:
    name: release
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: 現在の最新タグを取得
        id: cur_tag_var
        run: |
          git fetch --prune --unshallow
          echo "$(git tag --sort=creatordate | tail -n 1)" > CUR_TAG
          echo ::set-output name=cur_tag::$(cat CUR_TAG)

      - name: リリースタグを設定
        id: set_release_tag
        run: |
          echo "${{ steps.cur_tag_var.outputs.cur_tag }}"

          if [[ "${{ github.event.inputs.release_version }}" != '' ]]; then
            echo 'リリースバージョンが指定されました'
            echo "${{ github.event.inputs.release_version }}" > TAG_NAME
          else
            if [[ "${{ steps.cur_tag_var.outputs.cur_tag }}" =~ ^[0-9].+$ ]]; then
              echo 'カウントアップ可能なリリースタグです'
              CUR_VERSION=${{ steps.cur_tag_var.outputs.cur_tag }}
              VERSION=( ${CUR_VERSION//./ } )

              if ${{ github.event.inputs.major_release }}; then
                echo 'メジャーバージョン リリース'
                ((VERSION[-2]+=1))
                VERSION[-1]=0
              else
                echo 'マイナーバージョン リリース'
                ((VERSION[-1]+=1))
              fi

              echo $NEW_VERSION
              NEW_VERSION=$(IFS="."; echo "${VERSION[*]}")
              echo $NEW_VERSION > TAG_NAME
            else
              if [[ "${{ github.event.inputs.release_version }}" != '' ]]; then
                echo 'カウントアップできないリリースタグのため、指定されたリリースバージョンでリリースします'
                echo "${{ github.event.inputs.release_version }}" > TAG_NAME
              else
                echo 'リリースタグが不正です。リリースバージョンを指定してリリースしてください。'
                exit 1
              fi
            fi
          fi
          echo ::set-output name=tag_name::$(cat TAG_NAME)

      - name: call release slack message creator
        id: call-release-slack-message-creator
        run: |
          SLACK_MESSAGE="
          これからフロントリリースを始めます。
          バージョン: ${{ steps.cur_tag_var.outputs.cur_tag }} -> ${{ steps.set_release_tag.outputs.tag_name }}"
          echo "::set-output name=slack-message::${SLACK_MESSAGE//$'\n'/'%0A'}"

      - name: Slack Release Notification on Success
        uses: rtCamp/action-slack-notify@v2.0.2
        env:
          SLACK_USERNAME: フロントリリース
          SLACK_CHANNEL: [YOUR_CHANNEL_NAME]
          SLACK_LINK_NAMES: true
          SLACK_TITLE: フロントリリース
          SLACK_COLOR: "#36C5F0"
          SLACK_MESSAGE: "${{ steps.call-release-slack-message-creator.outputs.slack-message }}"

      - name: relase job
        run: |
          echo "リリース中..."
          echo "hogehoge" > release_target.txt

      - name: リリースタグをgitにpush
        id: set_git_tag
        run: |
          echo "${{ steps.set_release_tag.outputs.tag_name }}" > TARGET_TAG_NAME
          cat TARGET_TAG_NAME
          git tag $(cat TARGET_TAG_NAME)
          git push origin $(cat TARGET_TAG_NAME)
          echo ::set-output name=target_tag_name::$(cat TARGET_TAG_NAME)

      - name: get Tag ver
        id: get_tag_ver
        run: |
          echo "$(git tag --sort=creatordate | tail -n 1)" > CUR_TAG
          echo ::set-output name=cur_tag::$(cat CUR_TAG)
          echo "最新タグ：$(cat CUR_TAG)"
          echo "$(git tag --sort=creatordate | tail -n 2 | head -n1)" > PRE_TAG
          echo ::set-output name=pre_tag::$(cat PRE_TAG)
          echo "一つ前のタグ：$(cat PRE_TAG)"

      - name: release diff get
        id: commit_diff
        run: |
          echo -e ":clock10: release date: $(TZ=Asia/Tokyo date +%Y-%m-%d-%H%M%S)\n" > DIFF_COMMIT
          echo "$(git log --merges --first-parent --reverse --pretty=format:"* %b" ${{ steps.get_tag_ver.outputs.pre_tag }}..${{ steps.get_tag_ver.outputs.cur_tag }})" >> DIFF_COMMIT
          echo "差分：$(cat DIFF_COMMIT)"
          echo ::set-output name=diffCommit::$(cat DIFF_COMMIT)

      - name: Create release
        id: create_release
        uses: actions/create-release@v1.1.4
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ steps.get_tag_ver.outputs.cur_tag }}
          release_name: Release ${{ steps.get_tag_ver.outputs.cur_tag }}
          body_path: ./DIFF_COMMIT
          draft: false
          prerelease: false

      - name: Upload Release Asset
        id: upload-release-asset
        uses: actions/upload-release-asset@v1.0.1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }}
          asset_path: ./release_target.txt
          asset_name: release_target.txt
          asset_content_type: text/plain

      # テスト成功時はこちらのステップが実行される
      - name: Slack Notification on Success
        if: success()
        uses: rtCamp/action-slack-notify@v2.0.2
        env:
          SLACK_USERNAME: 商用環境デプロイ通知
          SLACK_CHANNEL: [YOUR_DEV_CHANNEL_NAME]
          SLACK_TITLE: 商用環境へのデプロイが完了しました。
          SLACK_COLOR: good

      - name: release success slack message creator
        if: success()
        id: release-success-slack-message-creator
        run: |
          SLACK_MESSAGE="
          :rocket: フロントリリースが完了しました。
          :white_check_mark: バージョン: ${{ steps.cur_tag_var.outputs.cur_tag }} -> ${{ steps.set_release_tag.outputs.tag_name }}
          :notebook_with_decorative_cover: リリースノート: <https://github.com/kansukeito/actions-test/releases>

          リリース内容は以下のとおりです。
          $(cat DIFF_COMMIT)"
          echo ${SLACK_MESSAGE}
          echo "$(cat DIFF_COMMIT)"
          echo "::set-output name=slack-message::${SLACK_MESSAGE//$'\n'/'%0A'}"

      - name: Slack Release Notification on Success
        if: success()
        uses: rtCamp/action-slack-notify@v2.0.2
        env:
          SLACK_USERNAME: フロントリリース
          SLACK_CHANNEL: [YOUR_CHANNEL_NAME]
          SLACK_TITLE: フロントリリース
          SLACK_COLOR: "#36C5F0"
          SLACK_MESSAGE: "${{ steps.release-success-slack-message-creator.outputs.slack-message }}"

      # テスト失敗時はこちらのステップが実行される
      - name: Slack Notification on Failure
        uses: rtCamp/action-slack-notify@v2.0.2
        if: failure()
        env:
          SLACK_USERNAME: 商用環境デプロイ通知
          SLACK_CHANNEL: [YOUR_DEV_CHANNEL_NAME]
          SLACK_TITLE: 商用環境へのデプロイに失敗しました。
          SLACK_COLOR: danger
```

## 設定必要な secret 情報

| 項目              | 何を設定するのか            |
| ----------------- | --------------------------- |
| SLACK_WEBHOOK_URL | Slack incomming Webhook url |

# 各 step の説明

## リリース時のバージョン指定

```yml
on:
  workflow_dispatch:
    inputs:
      major_release:
        description: メジャーバージョンリリース カウントアップ(optional)
        type: boolean
        default: false
        required: false

      release_version:
        description: リリースバージョンの明示的な指定(optional)  入力した場合はmajor_releaseオプションは無視される。
        type: string
        required: false
```

inputs を記述すると pipeline の手動実行時に引数を設定できるようになります。

![002_run_workflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/d4c832bd-434d-7ea0-4ee0-11f47e64659b.png)


ここで設定している項目は２つです。
どちらもオプション設定で、特に何もいじらない場合はマイナーリリースのバージョンが自動でカウントアップされる想定です。

| 項目               | タイプ  | 設定した場合                                                                                    | 設定しない場合                                   |
| ------------------ | ------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| メジャーリリース   | boolean | メジャーリリースバージョンがカウントアップされる                                                | マイナーリリースバージョンがカウントアップされる |
| バージョン手動指定 | string  | 任意のバージョン文字列でリリースを行う。<br/>メジャーリリースにチェックが入っていても無視される | -                                                |

## 最新タグを取得

```yml
- name: 現在の最新タグを取得
  id: cur_tag_var
  run: |
    git fetch --prune --unshallow
    echo "$(git tag --sort=creatordate | tail -n 1)" > CUR_TAG
    echo ::set-output name=cur_tag::$(cat CUR_TAG)
```

現在リリースされている最新のタグを取得します。
`--sort=creatordate`のオプションを使うことでタグの文字列に関係なくタグを打った順で並べて表示してくれるので、最新の 1 件のみパイプで抽出しています。

## リリース用のタグ文字列を決定

```yml
- name: リリースタグを設定
  id: set_release_tag
  run: |
    echo "${{ steps.cur_tag_var.outputs.cur_tag }}"

    if [[ "${{ github.event.inputs.release_version }}" != '' ]]; then
      echo 'リリースバージョンが指定されました'
      echo "${{ github.event.inputs.release_version }}" > TAG_NAME
    else
      if [[ "${{ steps.cur_tag_var.outputs.cur_tag }}" =~ ^[0-9].+$ ]]; then
        echo 'カウントアップ可能なリリースタグです'
        CUR_VERSION=${{ steps.cur_tag_var.outputs.cur_tag }}
        VERSION=( ${CUR_VERSION//./ } )

        if ${{ github.event.inputs.major_release }}; then
          echo 'メジャーバージョン リリース'
          ((VERSION[-2]+=1))
          VERSION[-1]=0
        else
          echo 'マイナーバージョン リリース'
          ((VERSION[-1]+=1))
        fi

        echo $NEW_VERSION
        NEW_VERSION=$(IFS="."; echo "${VERSION[*]}")
        echo $NEW_VERSION > TAG_NAME
      else
        if [[ "${{ github.event.inputs.release_version }}" != '' ]]; then
          echo 'カウントアップできないリリースタグのため、指定されたリリースバージョンでリリースします'
          echo "${{ github.event.inputs.release_version }}" > TAG_NAME
        else
          echo 'リリースタグが不正です。リリースバージョンを指定してリリースしてください。'
          exit 1
        fi
      fi
    fi
    echo ::set-output name=tag_name::$(cat TAG_NAME)
```

`input`で指定された引数を考慮してリリース用のタグを決定します。
メジャーバージョンかマイナーバージョンか判断し、カウントアップ可能であればカウントアップを行います。
現在の最新リリースタグがカウントアップができないようなタグ(日付で管理されたタグなど)で、バージョン手動指定されていない場合はわざとパイプラインを落とします。

## リリース前事前通知

```yml
- name: call release slack message creator
  id: call-release-slack-message-creator
  run: |
    SLACK_MESSAGE="
    これからフロントリリースを始めます。
    バージョン: ${{ steps.cur_tag_var.outputs.cur_tag }} -> ${{ steps.set_release_tag.outputs.tag_name }}"
    echo "::set-output name=slack-message::${SLACK_MESSAGE//$'\n'/'%0A'}"

- name: Slack Release Notification on Success
  uses: rtCamp/action-slack-notify@v2.0.2
  env:
    SLACK_USERNAME: フロントリリース
    SLACK_CHANNEL: [YOUR_CHANNEL_NAME]
    SLACK_LINK_NAMES: true
    SLACK_TITLE: フロントリリース
    SLACK_COLOR: "#36C5F0"
    SLACK_MESSAGE: "${{ steps.call-release-slack-message-creator.outputs.slack-message }}"
```

`rtCamp/action-slack-notify`という actions 拡張を使って Slack へ通知しています。
メッセージの内容に改行などを使いたかったので、直前の step でメッセージを作ってから Slack に通知しています。

例としてこんな感じの通知が Slack に流れます。
![003_pre_release_slack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/077c43de-087d-edda-4d19-4cb37c542c62.png)


## リリース作業中

```yml
- name: relase job
  run: |
    echo "リリース中..."
    echo "hogehoge" > release_target.txt
```

このタイミングでリリース作業を行います(test, デプロイとか・・・)
今回はサンプルとしてリリースノートに保存する資材のシミュレーションとして text ファイルを吐くだけにしてます。

## リリース完了後、タグを更新

```yml
- name: リリースタグをgitにpush
  id: set_git_tag
  run: |
    echo "${{ steps.set_release_tag.outputs.tag_name }}" > TARGET_TAG_NAME
    cat TARGET_TAG_NAME
    git tag $(cat TARGET_TAG_NAME)
    git push origin $(cat TARGET_TAG_NAME)
    echo ::set-output name=target_tag_name::$(cat TARGET_TAG_NAME)
```

今回のリリース用に設定したリリースバージョンタグを github に反映しています。

## 最新のリリースバージョンタグと前回のリリースバージョンタグの間の PR の差分を取得する

```yml
- name: get Tag ver
  id: get_tag_ver
  run: |
    echo "$(git tag --sort=creatordate | tail -n 1)" > CUR_TAG
    echo ::set-output name=cur_tag::$(cat CUR_TAG)
    echo "最新タグ：$(cat CUR_TAG)"
    echo "$(git tag --sort=creatordate | tail -n 2 | head -n1)" > PRE_TAG
    echo ::set-output name=pre_tag::$(cat PRE_TAG)
    echo "一つ前のタグ：$(cat PRE_TAG)"

- name: release diff get
  id: commit_diff
  run: |
    echo -e ":clock10: release date: $(TZ=Asia/Tokyo date +%Y-%m-%d-%H%M%S)\n" > DIFF_COMMIT
    echo "$(git log --merges --first-parent --reverse --pretty=format:"* %b" ${{ steps.get_tag_ver.outputs.pre_tag }}..${{ steps.get_tag_ver.outputs.cur_tag }})" >> DIFF_COMMIT
    echo "差分：$(cat DIFF_COMMIT)"
    echo ::set-output name=diffCommit::$(cat DIFF_COMMIT)
```

最新のバージョンでリリースした内容を取得します。
この例では PR のタイトルの一覧を取得しています。
内容は DIFF_COMMIT という一時ファイルに書き込まれています。

## リリースノートの作成とリリースした資材の格納

```yml
- name: Create release
  id: create_release
  uses: actions/create-release@v1.1.4
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  with:
    tag_name: ${{ steps.get_tag_ver.outputs.cur_tag }}
    release_name: Release ${{ steps.get_tag_ver.outputs.cur_tag }}
    body_path: ./DIFF_COMMIT
    draft: false
    prerelease: false

- name: Upload Release Asset
  id: upload-release-asset
  uses: actions/upload-release-asset@v1.0.1
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  with:
    upload_url: ${{ steps.create_release.outputs.upload_url }}
    asset_path: ./release_target.txt
    asset_name: release_target.txt
    asset_content_type: text/plain
```

- `actions/create-release`という actions 拡張でリリースノートを自動作成します。
- `body_path`に内容が記載された`DIFF_COMMIT`ファイルを指定することでリリースノートの内容を指定します。
- `actions/upload-release-asset`という actions 拡張でリリースノートに資材を配置します。
- リリースノートを作成した step で upload_url が生成されているので、それを指定します。

資材を複数格納したい場合は、配置する数分`actions/upload-release-asset`を呼び出してあげれば格納することができます。

![009_release_note2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/8b32ccec-b123-3845-f935-064234a6c4e4.png)


## リリース完了通知(開発チーム向け)

```yml
- name: Slack Notification on Success
  if: success()
  uses: rtCamp/action-slack-notify@v2.0.2
  env:
    SLACK_USERNAME: 商用環境デプロイ通知
    SLACK_CHANNEL: [YOUR_DEV_CHANNEL_NAME]
    SLACK_TITLE: 商用環境へのデプロイが完了しました。
    SLACK_COLOR: good
```

`if: success()`によって、パイプラインが成功した時のみ動作する step です。
`SLACK_CHANNEL`に開発チーム向けのチャンネル名を指定しています。
リリース前通知の時と同様の方法で Slack に通知を行います。
例としてこんな感じの通知が Slack に流れます。

![005_done_release_dev_slack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/56a6416e-0bd9-01cd-39e6-8e8a7d9654bf.png)


## リリース完了通知

```yml
- name: release success slack message creator
  if: success()
  id: release-success-slack-message-creator
  run: |
    SLACK_MESSAGE="
    :rocket: フロントリリースが完了しました。
    :white_check_mark: バージョン: ${{ steps.cur_tag_var.outputs.cur_tag }} -> ${{ steps.set_release_tag.outputs.tag_name }}
    :notebook_with_decorative_cover: リリースノート: <https://github.com/kansukeito/actions-test/releases>

    リリース内容は以下のとおりです。
    $(cat DIFF_COMMIT)"
    echo ${SLACK_MESSAGE}
    echo "$(cat DIFF_COMMIT)"
    echo "::set-output name=slack-message::${SLACK_MESSAGE//$'\n'/'%0A'}"

- name: Slack Release Notification on Success
  if: success()
  uses: rtCamp/action-slack-notify@v2.0.2
  env:
    SLACK_USERNAME: フロントリリース
    SLACK_CHANNEL: [YOUR_CHANNEL_NAME]
    SLACK_TITLE: フロントリリース
    SLACK_COLOR: "#36C5F0"
    SLACK_MESSAGE: "${{ steps.release-success-slack-message-creator.outputs.slack-message }}"
```

`if: success()`によって、パイプラインが成功した時のみ動作する step です。
リリース前通知の時と同様の方法で Slack に通知を行います。
例としてこんな感じの通知が Slack に流れます。
![010_done_release_slack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/82cf8e4f-76c7-ad33-1975-c880c5d485ec.png)


## リリース失敗通知(開発チーム向け)

```yml
- name: Slack Notification on Failure
  uses: rtCamp/action-slack-notify@v2.0.2
  if: failure()
  env:
    SLACK_USERNAME: 商用環境デプロイ通知
    SLACK_CHANNEL: [YOUR_DEV_CHANNEL_NAME]
    SLACK_TITLE: 商用環境へのデプロイに失敗しました。
    SLACK_COLOR: danger
```

`if: failure()`によって、パイプラインが失敗した時のみ動作する step です。
`SLACK_CHANNEL`に開発チーム向けのチャンネル名を指定しています。
リリース前通知の時と同様の方法で Slack に通知を行います。
例としてこんな感じの通知が Slack に流れます。
![006_err_release_slack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/e4546f88-4295-9d5b-9bbe-63d85a6fcbbc.png)


# まとめ

- このパイプラインによって、リリースの際にかかる地味な手作業が無くなってかなり快適になりました。
- リリースノートへの記載あたりはもうちょっとどうにかできそうな気がします(release.yml を使うみたいに bugfix や feature などそれぞれで分けて記載できるようにするとか・・・)
- github actions自由度が高くて素晴らしいですね。:rocket::rocket::rocket::rocket::rocket: