リリースビルドやデプロイのタイミングでGitHub CLI使ってリリースノートを書く

今のプロジェクトではスクラムで開発を進めていますが、スプリントの終わりに毎回リリースノートを書くのが面倒だったので、リリースビルドやデプロイ時の終わりにシェルスクリプト実行して登録しています。

前提としてGitHub上で以下のようにスプリント毎のタスクを管理しています。

* スプリント期間でマイルストーンを置いている
* 作業は全てIssuesで管理されていてマイルストーンが割り当てられている
* 主要な機能には専用のラベルが追加されている (サンプルでは`sprint-feature`というラベル）

サンプルプロジェクトは [ここ(bu-tokumitsu/releasenote-script)](https://github.com/bu-tokumitsu/releasenote-script) にあります。
最終的に作成されるリリースノートは下の画像のようになります。


![スクリーンショット 2021-08-17 18.33.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/4c0daf86-0c5e-6936-c2a3-f3313fade00e.png)

主要な機能は主な変更点としてそのまま表示して、その他のIssuesやPullRequestなどは全て表示すると表示が長くなることが多々あるので折りたためるようにしています。

## 使用するには

[GitHub CLI](https://github.com/cli/cli)と[jq](https://stedolan.github.io/jq/download/)を入れる必要があります。

* この記事で使用したバージョン
    * GitHub CLI - v1.9.2
    * jq - v1.5

## GitHub CLIのログインを行う

ここはローカルで実行するかCI上で実行するかで手順が変わります。

<details>
<summary>ローカルで実行する</summary>
<div>

`gh auth login` 実行後に対話形式で進めます （行の末尾{}内は自分が選択した内容）

```
$ gh auth login
 --------------------------------------------------------------------------
 ? What account do you want to log into? {GitHub.com}
 ? What is your preferred protocol for Git operations? {HTTPS}
 ? Authenticate Git with your GitHub credentials? {Yes}
 ? How would you like to authenticate GitHub CLI? {Login with a web browser}

 ! First copy your one-time code: {ブラウザで認証コード入力を選択した場合はここにコードが表示される}
 - Press Enter to open github.com in your browser...
 ✓ Authentication complete. Press Enter to continue...

 - gh config set -h github.com git_protocol https
 ✓ Configured git protocol
 ✓ Logged in as {githubユーザー名}
 --------------------------------------------------------------------------
```

</div>
</details>

<details>
<summary>CIで実行する</summary>
<div>

[GitHubの個人トークン](https://docs.github.com/ja/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token)をCIの環境変数に登録し、CI実行した時の処理で以下のようにログインを行います。

```
echo $GITHUB_TOKEN | gh auth login --with-token
```

</div>
</details>


## コード

```githubRelease.sh
#!/bin/sh

# リポジトリ情報
owner="bu-tokumitsu"
repository="releasenote-script"
repo="${owner}/${repository}"

# 実行ブランチ
releaseBranch="main"
branch=$(git rev-parse --abbrev-ref @)
if [ $releaseBranch != $branch ]; then
    echo "
    ${releaseBranch}ブランチで実行してください
    "
    exit 1
fi

# 引数1: マイルストーン名、 引数2: アプリバージョン
milestoneName=$1
appVersion=$2
filterLabel="sprint-feature"

if [ -z "$milestoneName" ] || [ -z "$appVersion" ]; then
    echo "
    引数が足りません
    1:マイルストーン名 => $milestoneName
    2:リリースバージョン => $appVersion
    "
    exit 1
else
    echo "
    ${milestoneName} (${appVersion}) のリリース開始
    "
fi


# Issues
issuesJson=$(gh issue list -R ${repo} --search "milestone:${milestoneName}" -s "closed" --json "number,title,url,labels")
# Issueの中でもスプリント対応（追加機能）として起票されたIssueのみをまとめる
mainIssues=$(echo $issuesJson | jq -r 'sort_by(.number) | map(select(.labels[].name == "'$filterLabel'")) | .[] | "* [\(.title)](\(.url))" | @text')
# その他のIssues
otherIssuesFilter=$(echo $issuesJson | jq -r 'sort_by(.number) | map(select(.labels[].name != "'$filterLabel'"))')
otherIssuesCnt=$(echo $otherIssuesFilter | jq -r 'length')
otherIssues=$(echo $otherIssuesFilter | jq -r '.[] | "* [\(.title)](\(.url))" | @text')

# PullRequests
prJson=$(gh pr list -R ${repo} --search "milestone:${milestoneName}" -s "closed" --json "number,title,url")
prCnt=$(echo $prJson | jq -r 'length')
prText=$(echo $prJson | jq -r 'sort_by(.number) | .[] | "* [\(.title)](\(.url))" | @text')

# リリースノート
# タイトル
releaseTitle="${milestoneName} リリース"
# 本文
mainRelText="# 主な変更点\n\n${mainIssues}\n\n"
otherRelText="<details>\n<summary>その他のIssuesを表示（${otherIssuesCnt}）</summary>\n\n${otherIssues}\n</details>\n\n"
prRelText="<details>\n<summary>PullRequestsを表示（${prCnt}）</summary>\n\n${prText}\n</details>\n\n"
releaseBody=$(echo "${mainRelText}${otherRelText}${prRelText}")


# タグ登録(直近のタグに同名のものがあれば削除して付け直し)
latestTag=$(git describe --abbrev=0)
if [ "$latestTag" = "$appVersion" ]; then
    echo "
    ${latestTag} タグが既に登録されているので付け直しの為、一度削除します
    "
    git tag -d $latestTag
    git push -d origin $latestTag
fi
echo "
    ${appVersion} タグを登録します
"
git tag -a $appVersion -m "${milestoneName}リリース"
git push --tags

# リリースノート
latestReleaseInfo=$(gh release list -R ${repo} -L 1)
if [[ $latestReleaseInfo =~ $appVersion ]]; then
    echo "
    ${latestReleaseInfo}
    が既に登録されているので削除します
    "
    gh release delete -R $repo $appVersion -y 
fi
echo "
    ${releaseTitle} を登録します
"
resultUrl=$(gh release create -R $repo $appVersion -t "${releaseTitle}" -n "${releaseBody}")

echo "
    ${milestoneName} (${appVersion}) のリリース完了
    ${resultUrl}
"
```

#### 実行

```
ex) $ sh ./githubRelease.sh "Milestone1" v1.0.0
$ sh ./githubRelease.sh ${MILESTONE_NAME} ${RELEASE_VERSION}
```

`gh release` 後の内容の更新方法がなさそうだったので１度削除してから登録しています。
タグの付け直しとかも含めてこの辺の判定は直近でしか見ていません。

あとIssuesのjqコマンドのselectで絞っているところも **Issuesには必ず何かしらのラベルが付いている** という前提で組んでいるのでIssuesにラベルを加えていないとデータが漏れると思います。

CIで実行する場合はマイルストーン名とバージョン情報も引っ張れるようにする必要もあります。
自分の場合はアプリケーションの構成ファイルから情報取って渡すようにしています。

## ハマったところ

当初[GitHub CLIのドキュメント(gh_issue_list)](https://cli.github.com/manual/gh_issue_list)で `gh issue list` でマイルストーン名で絞って取得するのは `--milestone` オプションを使えばできそうでしたがこの方法ではうまく絞れませんでした。

解決方法はこちらのIssuesに記載ありますが `--search 'milestone:"マイルストーン名"'` を使用することで取得できます。
[cli/cli gh issue list ... --milestone [value] -> illegal base64 data at input byte 2](https://github.com/cli/cli/issues/4040)