GitHub Actionsでブランチに合わせてリリース用PRを自動生成する

2021/12/30に公開

きっかけ

スプリントで実装した内容をリリースする際、PRを毎回手作業で作成しているのですが、「ちょっと手間だな。自動化したいな〜」と思っていました。
年末になって時間ができたので、折角なら勉強も兼ねてアクションを自作しようと思います。

要件

  • developにマージされたら、stageへのPRが自動で生成されること
  • stageにマージされたら、mainへのPRが自動で生成されること
  • PRのタイトルやボディが指定できること
  • 既にリリース用PRが存在する場合はスキップすること

完成形はこちら

いきなりですが、完成形のアクションはこちらになります。

完成版
name: Create a pull request for release.

on:
  push:
    branches: [ stage, develop ]

jobs:
  create-release-pr:
    runs-on: ubuntu-latest

    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

    steps:
      - uses: actions/checkout@v2

      # リリース用PRが既に存在するかどうかをチェック
      - name: Check if pr exists
        id: check_pr
        run: |
          pr_title=${{ (github.ref == 'refs/heads/stage' && 'Stage') || 'Develop' }}
          base_branch=${{ (github.ref == 'refs/heads/stage' && 'main') || 'stage' }}
          echo "::set-output name=count::$(gh pr list -S ${pr_title}' in:title' -B $base_branch | wc -l)"
          echo "::set-output name=pr_title::$pr_title"
          echo "::set-output name=base_branch::$base_branch"
      # リリース用PRを作成
      - name: Create release pr
        if: ${{ steps.check_pr.outputs.count == 0 }}
        run: |
          gh pr create -B ${{ steps.check_pr.outputs.base_branch }} -t ${{ steps.check_pr.outputs.pr_title }} -b ""

内容について順に確認していきましょう。

developにマージされた際、自動でstageへのPRを作成する

マージされたタイミングでPRを作成したいです。

hubコマンドを使う方法もあるのですが、今回はGitHub CLIを使っていこうと思います。

GitHub CLIでPRを作成するコマンドは以下です。

  gh pr create [flags]

詳しくは公式ドキュメントに記載されているので、気になる方はそちらをご確認ください。
https://cli.github.com/manual/gh_pr_create

まずはシンプルなアクションを作成してみました。

PR作るだけの状態
name: Create a pull request for release.

on:
  push:
    branches: [ develop ]

jobs:
  create-release-pr:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: Create release pr
        run: gh pr create -B "stage" -t "Develop" -b ""
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

ここでdevelopブランチにプッシュされた際に発火するようにしています。

  push:
    branches: [ develop ]

続いて、GitHub CLIコマンドでPRを新規作成していきましょう。

ここではstageブランチに対して、タイトルは"Develop"、ボディが空のPRを作成するようにしています。

  - name: Create release pr
    run: gh pr create -B "stage" -t "Develop" -b ""

-b, --body <string>
Body for the pull request
B, --base <branch>
The branch into which you want your code merged
-t, --title <string>
Title for the pull request

https://cli.github.com/manual/gh_pr_create

最後は環境変数の設定です。

GitHub CLIを使うにはログインが必要なので、ここで設定をしています。

  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

実際に作成されたPRがこちら。

ちなみに、Draft用のオプションもありますが、個人(GitHub Free)のプライベートリポジトリではドラフトPRの作成はできません。

▶︎ Run gh pr create -B "stage" -d -t "Develop" -b ""
pull request create failed: GraphQL: Draft pull requests are not supported in this repository. (createPullRequest)
Error: Process completed with exit code 1.

Draft pull requests are available in public repositories with GitHub Free for organizations and legacy per-repository billing plans, and in public and private repositories with GitHub Team, GitHub Enterprise Server 2.17+, and GitHub Enterprise Cloud.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests#draft-pull-requests

GitHub Teamなどを使っている場合は、以下のように -d オプションを付与するとDraft状態でPRを作成できます。

  - name: Create draft pr
    run: gh pr create -d -B "stage" -t "Develop" -b ""

d, --draft
Mark pull request as a draft

これで最低限PRは作成できるのですが、これだけだとdevelop→stageへのPRしか作成できません。
実際のリリースではstage→main、main→releaseなど、他にもパターンがあると思います。
ここは動的に変えられるようにしたいところです。

また、先ほどのアクションでは既にPRが存在している場合にエラーとなってしまいます。

▶︎ Run gh pr create -B "stage" -t "Develop" -b ""
a pull request for branch "develop" into branch "stage" already exists:
{PRのURL}
Error: Process completed with exit code 1.

これらの問題を解消するため、もう少し工夫をしていきましょう。

PRのマージ先を動的に切り替える

PRのマージ先を判定するには、このアクションがどのブランチで実行されたのかを知らなければなりません。

ブランチ名を取得する方法はいくつかありますが、今回は github.ref を使いましょう。
github.ref の中には refs/heads/{hoge} という形でブランチ名が格納されています。
github.ref == 'refs/heads/stage' などとすることで、ブランチ名の判別が可能です。

他にもスマートな取得の仕方もありますので、お好みでどうぞ〜

https://zenn.dev/masaaania/articles/c930f2f755a577#タグ・ブランチ名を取得する

実行されたブランチがstageかどうかを判定し、マージ先を切り替える処理は以下となります。
ついでにRPのタイトルも切り替えるようにしました。

マージ先・タイトルの切り替えあり
  - name: Check if pr exists
    id: check_pr
    run: |
      pr_title=${{ (github.ref == 'refs/heads/stage' && 'Stage') || 'Develop' }}
      base_branch=${{ (github.ref == 'refs/heads/stage' && 'main') || 'stage' }}
      echo "::set-output name=pr_title::$pr_title"
      echo "::set-output name=base_branch::$base_branch"
  - name: Create release pr
    run: |
      gh pr create -B ${{ steps.check_pr.outputs.base_branch }} -t ${{ steps.check_pr.outputs.pr_title }} -b ""

以下の箇所がPRのタイトルとマージ先のブランチを判別している処理です。

  pr_title=${{ (github.ref == 'refs/heads/stage' && 'Stage') || 'Develop' }}
  base_branch=${{ (github.ref == 'refs/heads/stage' && 'main') || 'stage' }}

ここでは三項演算子のように処理を記載しています。
({条件式} && {TRUEの場合の処理}) || {FALSEの場合の処理})のように実行されるので便利です。

今回は、ブランチがstageだった場合はpr_titleStageを、base_branchにはmainを格納するようにしています。
また、 ::set-output name={name}::{value} で次のステップで使用するパラメータを引き継げるように設定しています。

これでPRのマージ先を動的に変更できるようになりました。

既にリリース用PRが存在する場合はスキップする

最後は既にPRが存在している場合にエラーとなってしまう件の対応です。

事前にリリース用のPRが存在しているかどうかをチェックし、既に存在している場合はPR作成をスキップするように変更しましょう。

GitHub CLIにはPRの一覧を取得・検索するコマンドがありますので、それを使います。

  gh pr list [flags]

https://cli.github.com/manual/gh_pr_list

今回想定するリリース用PRの条件は以下です。

  • stage→mainへのPR、かつタイトルはStage
  • develop→stageのPR、かつタイトルはDevelop

検索条件としてはタイトルとPRの向き先でいけそうなので、以下のオプションを使います。

B, --base <string>
Filter by base branch
-S, --search <query>
Search pull requests with query

クエリの詳しい書き方については以下のページをご確認ください。

https://docs.github.com/ja/search-github/searching-on-github/searching-issues-and-pull-requests

というわけで、先ほど作成したアクションに追加したものがこちらになります。

リリース用PRの存在チェックあり
  - name: Check if pr exists
    id: check_pr
    run: |
      pr_title=${{ (github.ref == 'refs/heads/stage' && 'Stage') || 'Develop' }}
      base_branch=${{ (github.ref == 'refs/heads/stage' && 'main') || 'stage' }}
      echo "::set-output name=count::$(gh pr list -S ${pr_title}' in:title' -B $base_branch | wc -l)"
      echo "::set-output name=pr_title::$pr_title"
      echo "::set-output name=base_branch::$base_branch"
  - name: Create release pr
    if: ${{ steps.check_pr.outputs.count == 0 }}
    run: |
      gh pr create -B ${{ steps.check_pr.outputs.base_branch }} -t ${{ steps.check_pr.outputs.pr_title }} -b ""

以下の箇所がPRの検索処理になります。

  echo "::set-output name=count::$(gh pr list -S ${pr_title}' in:title' -B $base_branch | wc -l)"

GitHub CLIでPRの一覧を取得すると以下のように出力されるので、その行数を wc -l でカウントしています。

あとは取得したカウント数が0の場合(リリース用PRが存在しない場合)のみ、PR作成処理を実行するようにif文を追加しました。

  if: ${{ steps.check_pr.outputs.count == 0 }}

これで完成です!

以下が実際にPR作成処理がスキップされた際のログになります。
Create release prのstepがスキップできていますね。

作成処理の方も問題なしです。

おわりに

実際に自分でアクションを作ってみることでGitHub Actionsの理解が深まりました。(あと純粋に楽しかった!)
小さな改善ではありましたが、このような改善を積み重ねて、来年は業務の効率化をもっと進めていきたいと思います!

今回作ったアクションを再掲して終了です。

完成版
name: Create a pull request for release.

on:
  push:
    branches: [ stage, develop ]

jobs:
  create-release-pr:
    runs-on: ubuntu-latest

    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

    steps:
      - uses: actions/checkout@v2

      # リリース用PRが既に存在するかどうかをチェック
      - name: Check if pr exists
        id: check_pr
        run: |
          pr_title=${{ (github.ref == 'refs/heads/stage' && 'Stage') || 'Develop' }}
          base_branch=${{ (github.ref == 'refs/heads/stage' && 'main') || 'stage' }}
          echo "::set-output name=count::$(gh pr list -S ${pr_title}' in:title' -B $base_branch | wc -l)"
          echo "::set-output name=pr_title::$pr_title"
          echo "::set-output name=base_branch::$base_branch"
      # リリース用PRを作成
      - name: Create release pr
        if: ${{ steps.check_pr.outputs.count == 0 }}
        run: |
          gh pr create -B ${{ steps.check_pr.outputs.base_branch }} -t ${{ steps.check_pr.outputs.pr_title }} -b ""

参考にさせていただいた記事

素晴らしい記事をありがとうございました!

https://zenn.dev/snowcait/articles/0e430af5fb1e50

https://zenn.dev/masaaania/articles/c930f2f755a577

https://tech.medpeer.co.jp/entry/2021/06/24/100000

Discussion