エンジニアリング

GitHub Release を使って開発者体験のよいリリースフローを作る

GitHub Release を使って開発者体験のよいリリースフローを作る
#GitHub Actions#CI/CD#リリース#運用

はじめに

こんにちは、Acsim 開発チームの笹沢です。

AI 要件定義サービス Acsim のリリースフローを整備しました。目指したのは、開発者が普段どおり PR を main にマージしているだけで、バージョニングされた Draft Release が自動で出来上がっている状態です。リリース時にやることは Draft Release の内容を確認して Publish ボタンを押すだけです。本記事では、このフローを実現するために取り組んだ4つの取り組みを紹介します。

リリースフローの全体像

このフローで作成されるリリースノートの例です。AI による補足が付いているので、非開発者にも内容が伝わりやすくなっています。

リリースノートの例

それでは、各取り組みを詳しく見ていきます。

取り組み1: GitHub Release の Publish をデプロイのトリガーにする

本番環境へのデプロイのトリガーを GitHub Release の Publish にしました。

デプロイの起点としてよく使われるのは production ブランチへのマージです。シンプルで分かりやすい反面、リリース内容の記録はブランチには残らないため、いつ何をリリースしたかを管理したり追跡したりするには不向きです1

GitHub Release の Publish を本番デプロイの起点にすると、いつ・何をリリースしたかが Tag・リリースノートとともに GitHub 上に一元的に記録されます。

緊急ロールバック用に workflow_dispatch も用意しています。障害時に前の安定版の SHA を指定して手動デプロイできるようにしています。

# deploy-production.yml
on:
  release:
    types: [published]
  workflow_dispatch:
    inputs:
      sha:
        description: "デプロイ対象のコミット SHA"

取り組み2: Release Drafter で Draft Release を自動生成する

GitHub Release の Publish がデプロイの起点になったことで、次は「Draft Release の中身をどう管理するか」が重要になります。手動管理では漏れが発生するため、Release Drafter を導入しました。PR を main にマージするたびに Draft Release が自動で更新されます。

Release Drafter の役割は2つあり、それぞれトリガーが異なるため Workflow を分けています。

① PR タイトルからの自動ラベル付与

PR が作成・更新されたタイミングで、Conventional Commits に基づいてラベルを自動付与します。PR タイトルが feat: で始まれば feature ラベルが、fix: なら fix ラベルが付きます。

ラベル付与だけを行うため disable-releaser: true としています。

# .github/workflows/labeler.yml
on:
  pull_request:
    types: [opened, reopened, synchronize]

jobs:
  label:
    permissions:
      contents: read
      pull-requests: write
    runs-on: ubuntu-latest
    steps:
      - uses: release-drafter/release-drafter@v6
        with:
          # ラベル付与のみ。Draft Release の更新は行わない
          disable-releaser: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

ラベルと PR タイトルの対応は .github/release-drafter.ymlautolabeler で定義します。

# .github/release-drafter.yml(抜粋)
autolabeler:
  - label: "feature"
    title:
      - "/^feat:/i"
  - label: "fix"
    title:
      - "/^fix:/i"
  - label: "documentation"
    title:
      - '/^docs(\(.+\))?:/'

② Draft Release の自動更新とバージョン計算

PR が main にマージされたタイミングで、付与済みのラベルに基づいて Draft Release を自動で作成・更新します。Draft Release を開けば「次のリリースに何が含まれるか」が一目でわかります。

自動生成された Draft Release を見ると、PR のタイトル種別ごとに分類されていることが分かります。

Draft Release の例

バージョン番号もラベルから自動計算されます。

PR ラベルカテゴリバージョン変更
feature機能追加Minor
fixバグ修正Patch
chore, refactor, documentationその他Patch
major, breaking大規模改修Major

こちらのワークフローでは disable-autolabeler: true としています2

# .github/workflows/release-drafter.yml
on:
  push:
    branches:
      - main

jobs:
  update_release_draft:
    permissions:
      contents: write
      pull-requests: read
    runs-on: ubuntu-latest
    steps:
      - uses: release-drafter/release-drafter@v6
        with:
          # ラベル付与は labeler.yml で実行済みのため無効化
          disable-autolabeler: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

これにより、Draft Release を覗けばこれから何がリリースされるのか確認できるようになりました。

取り組み3: リリースブロック機構の導入

リリース時は Staging 環境で動作確認を行います。このとき新規の PR が main にマージされると、動作確認済みの内容とリリースされる内容に食い違いが生じます。

従来は Slack で「リリース作業中なのでマージしないでください」と呼びかけていましたが、Slack を見ていないメンバー(私です)が気付かずにマージしてしまうことがありました。そのため仕組みでブロックする必要がありました。

最初に検討したのは GitHub の Lock branch 機能です。しかし、この機能を有効にするには Admin 権限が必要であり、最小権限の原則から逸脱するため採用できませんでした。

release-lock ブランチで状態管理

Admin 権限なしで実現するアプローチとして、release-lock という特別なブランチの存在有無でロック状態を表現する方法を採用しました3

内容はシンプルで workflow_dispatchlock を選択すれば release-lock ブランチが作成され、unlock を選択すれば release-lock ブランチが削除されるだけです。

# .github/workflows/release-lock.yml
on:
  workflow_dispatch:
    inputs:
      action:
        type: choice
        options: [lock, unlock]

jobs:
  toggle:
    permissions:
      contents: write
    runs-on: ubuntu-latest
    steps:
      - name: Toggle lock
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          REPO="${{ github.repository }}"
          BRANCH="release-lock"

          if [ "${{ inputs.action }}" = "lock" ]; then
            SHA=$(gh api repos/$REPO/git/refs/heads/main --jq '.object.sha')
            gh api repos/$REPO/git/refs \
              -f ref="refs/heads/$BRANCH" -f sha="$SHA" 2>/dev/null || echo "Already locked"
          else
            gh api repos/$REPO/git/refs/heads/$BRANCH -X DELETE 2>/dev/null || echo "Already unlocked"
          fi

もう1つのワークフローが、release-lock ブランチの存在をチェックするものです。ブランチが存在すればジョブが失敗するようになっています。

# .github/workflows/check-release-lock.yml
on:
  pull_request:
    branches:
      - main
  merge_group:

jobs:
  check-release-lock:
    permissions:
      contents: read
    runs-on: ubuntu-latest
    steps:
      - name: Check if locked
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          if gh api repos/${{ github.repository }}/git/refs/heads/release-lock &>/dev/null; then
            echo "::error::Release lock is active. Please wait until the release is complete."
            exit 1
          fi
          echo "Not locked - OK to merge"

最後にステータスチェックにこのワークフローを追加します。

ステータスチェックに Release Lock を追加

これにより、PR のステータスチェックと Merge Queue の両方でロック状態(release-lock ブランチの有無)が検証されるため、リリース作業中に PR がマージされることはありません4

取り組み4: リリースノート作成を AI で支援する

Release Drafter が生成する Draft Release には PR タイトルの一覧が並びますが、そのままではリリースノートとしては情報が不足しています。各 PR の内容を読み解き、関連する変更をグループ化し、わかりやすい説明を書く作業は手間がかかります。

この作業を AI コーディングエージェントで自動化しています。以下のような手順書を用意し、AI に実行させるだけでリリースノートが完成します。

  1. gh release list で Draft Release のタグ名を取得し、gh release view で本文を取得する
  2. Draft Release に含まれる各 PR の内容を確認する
  3. 関連する PR をグループ化し、技術的な詳細とビジネス的な価値の両方を伝えるリリースノートを作成する

手順書にはリリースノートの構成例も定義しています。機能追加・バグ修正・その他のカテゴリに分け、各項目に対応する PR へのリンクを付ける形式です。AI はこの構成例に従ってリリースノートを生成するため、毎回一貫したフォーマットのリリースノートが得られます。

まとめ

本記事では、GitHub Release の Publish を起点としたリリースフローの整備について、4つの取り組みを紹介しました。

  1. GitHub Release の Publish をデプロイのトリガーに ── リリース履歴が Tag・リリースノートとして一元管理される
  2. Release Drafter で Draft Release を自動生成 ── PR のラベルに基づくカテゴリ分けとバージョン計算を自動化
  3. Release Lock の導入 ── リリース作業中の PR マージを仕組みでブロック
  4. AI でリリースノート作成を支援 ── PR の内容を読み解き、整理されたリリースノートを自動生成

もし GitHub Release 起点のリリースフローを整備したい方がいれば参考にしてみてください。

Footnotes

  1. Trunk Based Development では、Tag からのリリースは多くのチームにとって良い最適化であり、リリースブランチを完全に省略できると述べられています。また CircleCI のブログ では、Tag は一度作成するとコミットを追加できない固定の参照点であるのに対し、ブランチは開発とともに進み続ける独立した開発ラインであると解説されています。

  2. disable-autolabelerdisable-releaser のドキュメントが不足していたため、ささやかですが PR #1472 でコントリビュートしました。

  3. このアプローチは suzuki-shunsuke/lock-action を参考にしています。ブランチの存在有無でロック状態を管理するというアイデアを借用しました。

  4. 当初は merge_group トリガーのみで実装しましたが、Branch Protection の Required status checks に登録するには pull_request トリガーが必要でした。merge_group のみでは候補に表示されないため、pull_request トリガーを追加して解決しています。

この記事をシェア: