Github Actionsに色々任せよう -MergeされなかったPRからMilestoneを外す-

thumbnail

 

サントリーウエルネス DX推進部エンジニアリングGの青木です。

日々の業務でGithubを利用しているのですが、Github Actionsを使えば面倒なことを任せられることがわかり試行錯誤しながら導入しています。
前回、第一弾の記事を投稿したので今回はその続きとしてMergeされなかったPRからMilestoneを外す方法について紹介したいと思います。

課題感

Github Milestoneとは、そのリポジトリのバージョンに紐づく変更管理を容易にしてくれる機能です。
私のチームではMilestoneを使って変更管理をしているという話をしました。

GithubでPRのレビューやMergeを繰り返していく中で、とりあえずPRを作成したけどCloseしておこうといったPRも数多くあると思います。
そのPRがMilestoneに紐づいていた場合、CloseされたPRとしてMilestoneに残り続けることになります。
この場合、Milestoneで変更管理するにあたってMergeされていないPRまでMilestoneに紐づいている状態になるのでとても見づらくなります。
実際、CloseされたPRはMilestone上で見えなくしたいので手動で外す対応を取っていました。
これを自動化したいなというのが今回の課題です。

そうだ、Github Actionsに任せよう

このMilestone外しもGithub Actionsを使って行えます。
早速作っていきましょう。

構成

全体構成は下記の通りです。
PRがCloseされたのをトリガーとしてGithub Actionsが発火、Closeの状態を判別してMilestoneを外す。以上です。
これも簡単ですね。

Workflow

作成したワークフローはこんな感じです。
基本Pythonなどのスクリプトに処置を任せる系のワークフローは大体下記のような作りになると思います。

ここでポイントとなるのがif: ${{ !github.event.pull_request.merged }}の分岐を入れることです。
PRはCloseとMergeのステータスを持っており、CloseされたのにMergeされていない場合が今回Milestoneを外したい対象のPRとなります。
本当はGithub ActionsのトリガーでPRのステータスがCloseであるものだけを検知して発火すれば一番良いのですが、
types: closedの中にMerge/Closeが含まれてしまっているため、Workflowの中で判定を入れる必要があります。
この分岐をいれないとCloseされたすべてのPRがMilestoneから外れてしまうので注意が必要です。

※GithubのAPIを使ってPRをGETしたときのresponse.pull_request.merged_atがnullであることを見て判定可能ですが、わざわざPythonのスクリプトで処理させなくてもWorkflow内で判定できるなら無駄なスクリプトを実行しなくて良くなるのでオススメです。
詳しい記述方法はこちらをご覧ください。

name: Remove Milestone

on:
  pull_request:
    types: 
      - closed

permissions:
  issues: write
  pull-requests: write
  checks: write
  contents: read

jobs:
  remove-milestone:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install requests          

      - name: Remove milestone from PR if the PR is not merged
        if: ${{ !github.event.pull_request.merged }}
        env:
          PR_NUMBER: ${{ github.event.pull_request.number }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}
        run: python .github/scripts/remove_milestone.py

スクリプト

WorkflowでMergeされているかの分岐をかけているのでスクリプトではMilestone設定有無の分岐を追加して処理します。
GETで取得したレスポンスにMilestoneが設定されていれば、nullに更新するPATCHリクエストを送信します。

Githubが提供しているAPIの詳しい記述方法は公式ドキュメントをご覧ください。

import os
import requests

def remove_milestone(repo, pr_number, token):
    url = f"https://api.github.com/repos/{repo}/issues/{pr_number}"
    headers = {"Authorization": f"token {token}"}
    response = requests.get(url, headers=headers)

    if response.status_code == 200 and 'milestone' in response.json() and response.json()['milestone']:
        requests.patch(url, headers=headers, json={"milestone": None})
        print(f"Milestone removed from PR #{pr_number}")
    else:
        print(f"No milestone to remove for PR #{pr_number}")

if __name__ == "__main__":
    repo = os.environ["GITHUB_REPOSITORY"]
    pr_number = os.environ["PR_NUMBER"]
    token = os.environ["GITHUB_TOKEN"]
    remove_milestone(repo, pr_number, token)

動作確認

実行結果を見てみましょう。

PRがCloseされたかつMergeされている場合ではremove_milestone.pyは実行されない。

PRがCloseされたかつMergeされておらず、Milestoneがついている場合はMilestoneを外す。

PRがCloseされたかつMergeされていないが、Milestoneがnullの場合はPATCHが走らずにメッセージだけ出力される。

問題なさそうですね!
どのパターンであっても想定通りに処理されていること、かつ特定の場合のみMilestoneが外れていることを確認できました。

おわりに

ちょっとした作業であっても、量をこなさないといけない場合は積み重ねで面倒になることが多いです。
そのような場合はGithub Actionsなどのワークフローでなんとかできないか?と1回考えてみても良いかもしれません。
以上、参考になれば幸いです!