Release Drafterを利用してGitHubで日付ベースのタグを自動で付与する

# 概要
[Release Drafter](https://github.com/release-drafter/release-drafter) は main ブランチへの push 等をトリガーに GitHub の Release を自動で作成してくれる便利な GitHub Workflow です[^1]。この Release Drafter は Release を作成する際に自動でタグ名も生成してくれるのですが、タグ名の形式は `x.y.z` という SemVer 形式にしか対応していません。

リポジトリによっては `2022-02-14_1` のような日付ベースのタグ (CalVer) を付与したい場合があります。直接は CalVer に対応していない Release Drafter ですが、一工夫することで CalVer を付与することも可能なようです。そこでこの記事では、Release Drafter を利用して CalVer なタグ名を自動で付与する具体的な方法を紹介しています。

[^1]: Release Drafter自体の詳細は他に良い記事がいっぱいあるので本記事では触れていません。

# 背景
Release Drafter のリポジトリには、日付ベースのタグ (CalVer）を利用したいという issue (PR) が存在しています。

* https://github.com/release-drafter/release-drafter/issues/315
* https://github.com/release-drafter/release-drafter/pull/807

現時点では直接サポートされることはなさそうな気配ですが、代わりに各自の工夫で実現する方法が紹介されています。
具体的には、下記のように前段の step を追加して `date` コマンドでタグ名を生成する方法です。

```yaml
      # 説明に直接関係する部分のみを抜粋
      - name: Generate CalVer version
        id: calver
        run: |
          export VERSION=$(date "+%Y.%m.%d")
          echo ::set-output name=version::${VERSION}
      - uses: release-drafter/release-drafter@v5
        with:
          tag: ${{ steps.calver.outputs.version }}
```

ここで問題となるのが、生成されるタグ名が `2022.02.14` という形式のため、同じ日に２つ以上の Release を生成する場合にタグ名が重複するという点です。この問題を回避するためには、例えば `2022.02.14_1` のように、末尾に連番を付与する等の工夫が必要となります。

この問題点についても先ほどの issue で言及されているのですが、既存のタグ名を `gh release view` コマンドで取得できる･･･という部分のみで具体的にどのように実現すれば良いのか？ までは書かれていなかったので、本記事はその部分を紹介するものとなります。

# 内容
## 実装

背景で紹介した issue の内容を参考にすると、概ね下記の方針により `2022-02-14_1` のような日付＋連番のタグ名を付与することが可能そうです。

* Release Drafter の前段 step で date コマンドにより `2022-02-14` のような文字列を生成
* `gh release view` コマンドにより最新 Release のタグ名を取得、同じ日の Release が既にある場合は連番をインクリメント

これらの方針に従って、実直に GitHub Workflow を実装してみた例が下記となります。

```yaml:.github/workflows/release-drafter.yml
name: Release Drafter

on:
  push:
    branches:
      - main

jobs:
  update_release_draft:
    runs-on: ubuntu-latest
    steps:
      - name: Generate CalVer version
        id: calver
        run: |
          TODAY=$(date "+%Y-%m-%d")

          RELEASE_TAG=$(gh release --repo ${{github.repository}} view --json tagName --jq .tagName)
          if [ "${RELEASE_TAG}" = "" ]; then
            RELEASE_TAG="${TODAY}_0"
            echo "There is no release, use ${RELEASE_TAG}"
          fi

          TAGS=(${RELEASE_TAG//_/ })
          MAJOR_VERSION=${TAGS[0]}
          PATCH_VERSION=${TAGS[1]}

          PREFIX="${VERSION_PREFIX}${TODAY}"
          if [ "${MAJOR_VERSION}" = "${PREFIX}" ]; then
            PATCH_VERSION=$(expr ${PATCH_VERSION} + 1)
            echo "There is already a release for the same date, increment patch_version: ${PATCH_VERSION}"
          else
            PATCH_VERSION=1
          fi

          VERSION="${PREFIX}_${PATCH_VERSION}"

          echo ::set-output name=version::${VERSION}
          echo "Version set to ${VERSION}"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          VERSION_PREFIX: "v"
          TZ: "Asia/Tokyo"
      - uses: release-drafter/release-drafter@v5
        with:
          tag: ${{ steps.calver.outputs.version }}
          name: ${{ steps.calver.outputs.version }}
          version: ${{ steps.calver.outputs.version }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

この GitHub Workflow は下記のような仕様となります。

* `main` ブランチに push された際に動作（PRをmergeした場合もpushが発生するので動作します）
* 生成されるタグ名は `v2022-02-14_1` という形式
* 日付は日本時間（JST）を基準

Release Drafter を動作させるには、この GitHub Workflow ファイル (`.github/workflows/release-drafter.yml`) に加えて、設定ファイル (`.github/release-drafter.yml`) もリポジトリに設置する必要があります。

設定ファイルの詳細は本記事の主題ではないため、動作に必要な最小限の設定のみを下記に示しています。
今回の記事に直接関連するのは `tag-template` の部分です。公式のサンプルでは `v$RESOLVED_VERSION` という形で先頭に `v` がついているのですが、ここでは後述する理由により `v` なしで指定しています。

```yaml:.github/release-drafter.yaml
name-template: '$RESOLVED_VERSION '
tag-template: '$RESOLVED_VERSION'
template: |
  ## Changes

  $CHANGES
```

とりあえず動かしてみたいという場合は、以上の内容をリポジトリに設定すれば動作するはずです。

またサンプルで示した Workflow は概ね単なるシェルスクリプトなので、ファイルを見れば自明な部分が多いと思うのですが、実際にやってみると細かなポイントがいくつかありました。そこで以降では、各部分の詳細について説明していきます。

## 詳細

```yaml
on:
  push:
    branches:
      - main
```

Workflow の trigger 指定で main ブランチへの push 時に動作するように指定しています。

公式のサンプルでは Pull Request もトリガーに指定されていますが、そちらは Autolabeler という機能を使う場合に必要となるもので、今回の説明には不要だったので削除しています。

```yaml
jobs:
  update_release_draft:
    runs-on: ubuntu-latest
    steps:
      - name: Generate CalVer version
        id: calver
        run: |
          TODAY=$(date "+%Y-%m-%d")
          ...
```

ここで定義している step が今回の Workflow の主要な部分となります。

最初に date コマンドでタグの日付部分を生成しています。もしタグ形式を `20220214` や `2022.02.14` のような別の形式に変更したい場合は、この部分を変更することで実現できます。

```bash
          RELEASE_TAG=$(gh release --repo ${{github.repository}} view --json tagName --jq .tagName)
          if [ "${RELEASE_TAG}" = "" ]; then
            RELEASE_TAG="${TODAY}_0"
            echo "There is no release, use ${RELEASE_TAG}"
          fi
```

次に `gh release view` コマンドで最新リリースのタグ名を取得しています。
ここでのポイントは下記の2点です。

* `--jq` オプションでタグ名のみを取得する
* `--repo` オプションでリポジトリ名を指定する

`gh release view` コマンドには json 形式で特定の内容だけを取得する機能があります。またこの json に対して jq 形式のクエリーを実行できるようになっています。結果としてこのような指定をすることで、タグ名の文字列だけ[^2]を取り出すことが可能となります。

[^2]: `""` なしで取得できるので `jq` コマンドの `-r` を指定したのと同等の動作になるようです

またこの workflow では checkout を実行していないため、そのままでは `gh` コマンドがリポジトリ名を特定できずにエラーとなってしまいます。そのため `--repo` オプションでリポジトリ名を指定する必要があります。

```bash
          TAGS=(${RELEASE_TAG//_/ })
          MAJOR_VERSION=${TAGS[0]}
          PATCH_VERSION=${TAGS[1]}
```

前段で取得したタグ名を `_` で split して、日付部分と連番部分をそれぞれ取り出しています。

```bash
          PREFIX="${VERSION_PREFIX}${TODAY}"
          if [ "${MAJOR_VERSION}" = "${PREFIX}" ]; then
            PATCH_VERSION=$(expr ${PATCH_VERSION} + 1)
            echo "There is already a release for the same date, increment patch_version: ${PATCH_VERSION}"
          else
            PATCH_VERSION=1
          fi
```

最新Releaseのタグ名の日付部分が今日の日付と一致するか（つまり同じ日のReleaseが既にあるか）を判定しています。
もし同じ日のReleaseがある場合は、連番部分をインクリメントして利用します。

日付をタグ名に利用する場合 `v2022-02-01` のように先頭に prefix を付ける場合や `2022-02-01` のように prefix を付けない場合の両方が考えられます。本来の Release Draft のバージョンとしては数字部分だけを扱うようなのですが、それらの両方に対応するため `v` の部分についてもスクリプトで付与するようにしています。
（設定ファイルの `tag-template` で `v` なしの指定をしていたのは、スクリプト内で `v` も含めたタグ名を生成するためです）

```bash
          VERSION="${PREFIX}_${PATCH_VERSION}"

          echo ::set-output name=version::${VERSION}
          echo "Version set to ${VERSION}"
```

日付と連番を組み合わせてバージョン番号の文字列(`v2022-02-14_1`等)を生成します。
生成した文字列を `set-output` で保存しておき Release Drafter の実行時に利用します。

```yaml
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          VERSION_PREFIX: "v"
          TZ="Asia/Tokyo"
```

環境変数の指定でポイントになるのは下記です。

* `gh` コマンド用に `GITHUB_TOKEN` を指定する
* TZ で Timezone を指定する（`date` コマンドでの日付を制御するため）

TZ を指定しない場合は UTC が使われます。多くの場合、特定のタイムゾーンでの日付を基準にしたいはずなので、適切な Timezone を指定しておく必要があります。

```yaml
      - uses: release-drafter/release-drafter@v5
        with:
          tag: ${{ steps.calver.outputs.version }}
          name: ${{ steps.calver.outputs.version }}
          version: ${{ steps.calver.outputs.version }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

Release Drafter を実行する step です。
前段の step で output に設定された 日付＋連番 のタグ名を渡すことで、それを利用してくれるようになります。

# その他（細かな動作など）
## 複数日に渡って実行した場合

例えば下記のような例を考えてみます。

* 2022-02-14: main ブランチに最初のPRをmerge → `v2022-02-14_1` のタグ名で Release Draft が生成される
* 2022-02-15: main ブランチに次のPRをmerge → ？？？

通常のバージョン番号であれば、翌日になってもタグ名は変わらないので簡単なのですが、日付ベースのタグ名の場合はどうなるのか？ と考えると下記のようにいくつかの選択肢がある気がしてきます。

* `v2022-02-14_1` のまま内容だけが更新される
* `v2022-02-15_1` のタグ名で新規に Release Draft が生成される（`v2022-02-14_1` も残る）
* `v2022-02-15_1` のタグ名で既存の Release Draft が更新される

この点を実際に試してみたところ、一番最後の動作（既存 Draft の更新）となりました。

実装を確認していないので観測した範囲での理解となるのですが、Release Drafter は実行毎に最新の Release Draft [^3] を更新するという動作をしているようです。そのためタグ名がどのような形式かというのは関係なく、常に最新の Draft が設定に従って書き換わるという結果になるようです。

[^3]: Release Drafter が作成したものに限らず、手動で作成した Release Draft が最新であればそれが更新対象となる