📦

Monorepo におけるリリースフロー構築

2024/12/21に公開

はじめに

複数の Next.js アプリケーションを monorepo で管理しており、その際に作成したリリースフローについて解説します。
またリリースフロー・リリースノートの精度を高めるための仕組みについても解説します。

対象読者

  • monorepo を用いて開発しているエンジニア

構築したリリースフローについて

構築したリリースフローでの実際の流れを簡単に説明します。
最初にリリース対象とするアプリケーションとバージョンの上げ方(major / minor / patch)を選択します。

選択し、ワークフローを実行するとターゲットとしたアプリケーションの package.json が自動更新され、今回のリリースに関する Pull Request ・ Draft 状態のリリースノートが作成されます。

main から切ったリリース用のブランチが作成されており、この Pull Request に書いてある通りに、デプロイやリリースノートの publish をこなしていくとリリースが完了するというフローになっています。


生成された Draft 状態のリリースノート

(hotfix の場合は main ブランチでワークフローを起動するのではなく、前タグからワークフローを patch 指定で使用します)

リリースフローを構成する技術要素

本題に入り、このリリースフローをどのように実現しているかについて話していこうと思います。
今回のリリースフローは大きく以下の4つを組み合わせて作成されています。

  • GitHub CLI
  • Release Drafter
  • actions/labeler
  • branch protection rule

それぞれの役割についてまとめる前に全体像が見えた方がわかりやすいかと思うので、気になる方は以下のトグルよりワークフローを確認してみてください。

ワークフロー全体像
name: 00_App release
run-name: ${{ github.event.inputs.target }} - release

on:
  workflow_dispatch:
    inputs:
      target:
        description: "Target application"
        required: true
        type: choice
        options:
          - app_a
          - app_b
          - app_c
      semver:
        description: "Semantic version"
        required: true
        type: choice
        options:
          - major
          - minor
          - patch

permissions:
  contents: write
  pull-requests: write

jobs:
  create-pr-and-release-note:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
            
      # コミットできるユーザー設定
      - name: Set up Git user
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
      
      # リリースブランチ名を決める
      - name: Set release branch name
        id: set_release_branch_name
        run: |
          echo "release_branch=release_${{ github.event.inputs.target }}_$(TZ=Asia/Tokyo date +'%Y%m%d%H%M')" > "${GITHUB_OUTPUT}"

      - name: Create release branch
        run: |
          git checkout -b ${{ steps.set_release_branch_name.outputs.release_branch }}

      - name: Push release branch
        run: |
          git push origin ${{ steps.set_release_branch_name.outputs.release_branch }}

      - name: Create version update branch
        run: |
          git checkout -b chore/version-update
      
      # npm version で package.json のバージョンを更新
      # --no-git-tag-version でコミット時にタグを作成しない
      - name: version update
        id: version_update
        run: |
          cd apps/${{ github.event.inputs.target }}
          new_version=$(npm --no-git-tag-version version ${{ github.event.inputs.semver }})
          git add package.json
          git commit -m "${new_version}"
          echo "new_version=${new_version}" >> "${GITHUB_OUTPUT}"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Push version update branch
        run: |
          git push origin chore/version-update

      - name: Create version update PR
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         # ラベル付けすることで Release Drafter が正しく解釈したバージョンでリリースノートを作成できる
        run: |
          gh pr create \
            -B ${{ steps.set_release_branch_name.outputs.release_branch }} \
            -t "chore: ${{ github.event.inputs.target }} version update ${{ steps.version_update.outputs.new_version }}" \
            -a "${{ github.actor }}" \
            -b "Update version to ${{ steps.version_update.outputs.new_version }}" \
            -l "${{ github.event.inputs.semver }}" \
            -l "${{ github.event.inputs.target }}"

      # バージョン更新 PR をマージ
      - name: Merge version update PR
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh pr merge -s -d --admin
      
      # リリース後 main に反映するための PR
      - name: Check and Create Pull Request
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh pr create \
            -B main \
            -t "[${{ github.event.inputs.target }}] release ${{ steps.version_update.outputs.new_version }}" \
            -a "${{ github.actor }}" \
            -F ".github/release_pr_template.md"

      - name: Create release note
        uses: release-drafter/release-drafter@v6
        with:
          config-name: ${{ github.event.inputs.target }}-release-drafter.yml
          commitish: ${{ steps.set_release_branch_name.outputs.release_branch }} # リリースターゲットのブランチ名を指定
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

GitHub CLI

みなさんお馴染みかと思いますが GitHub CLI になります。

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

GitHub CLI はリリースフローの中で、 Pull Request の作成やマージなどを担います。
リリースフローを抜粋したものが以下になりますが、入力した targer (application), semver から package.json の自動更新・Pull Request 作成を以下で実現しています。

      # npm version で package.json のバージョンを更新
      - name: version update
        id: version_update
        run: |
          cd apps/${{ github.event.inputs.target }}
          new_version=$(npm --no-git-tag-version version ${{ github.event.inputs.semver }})
          git add package.json
          git commit -m "${new_version}"
          echo "new_version=${new_version}" >> "${GITHUB_OUTPUT}"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Push version update branch
        run: |
          git push origin chore/version-update
      - name: Create version update PR
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         # ラベル付けすることで Release Drafter が正しく解釈したバージョンでリリースノートを作成できる
        run: |
          gh pr create \
            -B ${{ steps.set_release_branch_name.outputs.release_branch }} \
            -t "chore: ${{ github.event.inputs.target }} version update ${{ steps.version_update.outputs.new_version }}" \
            -a "${{ github.actor }}" \
            -b "Update version to ${{ steps.version_update.outputs.new_version }}" \
            -l "${{ github.event.inputs.semver }}" \
            -l "${{ github.event.inputs.target }}"

      # バージョン更新 PR をマージ
      - name: Merge version update PR
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh pr merge -s -d --admin
      
      # リリース後 main に反映するための PR
      - name: Check and Create Pull Request
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh pr create \
            -B main \
            -t "[${{ github.event.inputs.target }}] release ${{ steps.version_update.outputs.new_version }}" \
            -a "${{ github.actor }}" \
            -F ".github/release_pr_template.md"

ポイントとしてバージョン更新をするための Pull Request に対して、 semver と同じラベルをつけています。
そうすることで次のリリースノート生成のタイミングで、Release Drafter の version-resolver で解釈され、正しいバージョンでのリリースノート生成につながります。

Release Drafter

Release Drafter を用いてリリースノートの自動作成を実現しています。
https://github.com/release-drafter/release-drafter

monorepo でのリリースノート生成であるため、特定のラベルがついた Pull Request からリリースノートを構築するための include-labels オプションは必須になります。

name-template: 'app_a@$RESOLVED_VERSION'
tag-template: 'app_a@$RESOLVED_VERSION'
tag-prefix: 'app_a@'
include-labels:
  - 'app_a' # 特定のラベルがついた Pull Request からリリースノートを構築する

template: |
  $CHANGES

categories: # 特定のラベルがついた Pull Request をそのタイトルの下に表示する
  - title: '新機能の追加'
    label: 'feat'
  - title: 'バグ修正'
    label: 'fix'
  - title: 'リファクタ'
    label: 'refactor'
  - title: 'ドキュメントの追加・修正'
    label: 'docs'
  - title: 'テストの追加・修正'
    label: 'test'
  - title: 'その他の変更'
    label: 'chore'

version-resolver: # ラベルによってバージョンをどのように上げるかを判断する
  major:
    labels:
      - 'major'
  minor:
    labels:
      - 'minor'
  patch:
    labels:
      - 'patch'
  default: minor

ワークフロー内では1番最後に実行しています。
これによって package.json の更新も含まれた内容でリリースノートが生成されます。

      - name: Create release note
        uses: release-drafter/release-drafter@v6
        with:
          config-name: ${{ github.event.inputs.target }}-release-drafter.yml
          commitish: ${{ steps.set_release_branch_name.outputs.release_branch }} # リリースターゲットのブランチ名を指定
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

(現状アプリケーションの個数分 **-release-drafter.yml という定義ファイルを用意しているため、1つで済む方法をご存知の方がいらっしゃれば教えていただきたいです 🙏)

actions/labler

Release Drafter でリリースノートを自動生成する際に、ラベルごとにカテゴリ分けをすることができ、それに役立つのが actions/labeler アクションになります。

https://github.com/actions/labeler

ブランチ名の prefix から Pull Request にラベルを付けることで、リリースノートにした際にみやすくカテゴリ分けされます。
リリースフローに直接関わるわけではありませんが、リリースノートの精度を上げるためには必須の機能です。

docs:
  - head-branch:
    - ^docs/
feat:
  - head-branch:
    - ^feat/
fix:
  - head-branch:
    - ^fix/
refactor:
  - head-branch:
    - ^refactor/
test:
  - head-branch:
    - ^test/
chore:
  - head-branch:
    - ^chore/

# ディレクトリごとにラベルを付与する
app_a:
  - changed-files:
    - any-glob-to-any-file:
      - apps/app_a/**
      - dockerfiles/app_a/**
      - packages/configs/src/app_a/**

app_b:
  - changed-files:
    - any-glob-to-any-file:
      - apps/app_b/**
      - dockerfiles/app_b/**
      - packages/configs/src/app_b/**


Pull Request についたラベルごとにカテゴリ分けされている

またディレクトリごとにもラベルを付与しています。これは Release Drafter でリリースノートを生成する際に、どのアプリケーションに関する Pull Request を取得するかに起因するため重要です。

branch protection rule

最後は GitHub の branch protection rule です。

https://docs.github.com/ja/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule

この branch protection rule を用いて、プッシュできるブランチに制約をかけています。
こちらもリリースフローに直接的に関わるわけではないですが、ブランチ名に制限を持たせることでラベル付与の精度を高めています。


上の命名規則にあったブランチのみ push できるように

設定はシンプルで上の画像のようにプッシュできるブランチ名を正規表現で絞っておき、 Restrict creations にチェックを入れるだけです。
この設定を加えることで、結果としてリリースノートが自然にカテゴリ分けされるようにしています。

まとめ

この記事では Monorepo におけるリリースフローの構築として、GitHub CLI / Release Drafter / actions/labeler / branch protection rule を用いたリリースフローを紹介しました。

このリリースフローを構築し、以下のような利点を感じています。

  • アプリケーションが増えてもリリースフローは1つで済むので管理が楽
  • actions/labeler と branch protection rule により、リリースノートのクオリティが担保できている
  • PR ベースでやり取りできるため、stg までは担当者 A, prd は担当者 B というケースでも問題なく対応できる
    • PR にデプロイしたログなどを残しておいたことで実際に引き継ぎがスムーズにいきました
  • PR に必要な手順が書いてあるので、初めてリリースするメンバーでも馴染みやすい

もし Monorepo でのリリースフローに困っている方がいらっしゃればぜひ参考にしてみてください。

参考

https://zenn.dev/stafes_blog/articles/ad5a84a301948b

Discussion