CI/CD

develop から main への自動 PR 生成ワークフローを構築する

GitHub Actions を使って develop ブランチから main ブランチへの自動 PR 生成、マージされた PR のチェックリスト表示、日付ベースのタグ付けを実現する方法を解説

develop ブランチをデフォルトブランチとし、main ブランチへのマージで本番リリースを行うワークフローを GitHub Actions で自動化する方法を解説します。

実現するワークフロー

  1. Feature Branch で開発
  2. develop への PR を作成
  3. PR がマージされる
  4. develop → main への PR を自動生成/更新
    • 新規作成時: PR タイトルは「[YYYYMMDD]リリース」
    • 既存 PR がある場合: PR 本文に新しい PR を追記
  5. 人間が PR タイトルの YYYYMMDD を実際の日付に変更
  6. PR をマージ
  7. main ブランチに YYYYMMDD_XX タグが付与される(同日複数回対応)

実装手順

ステップ 1: develop ブランチをデフォルトに設定

GitHub リポジトリの設定でデフォルトブランチを変更します:

  1. GitHub リポジトリの SettingsGeneral に移動
  2. Default branch セクションで develop を選択
  3. Update をクリック

ステップ 2: develop マージ時の自動 PR 生成ワークフロー

.github/workflows/create-release-pr.yml を作成します:

name: Create Release PR

on:
  push:
    branches:
      - develop

permissions:
  contents: write
  pull-requests: write

jobs:
  create-release-pr:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Check existing PR
        id: check
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # 既存のオープンな PR を確認(develop → main)
          # gh pr view を使用して直接確認
          if gh pr view develop --json number,body,state 2>/dev/null | jq -e '.state == "OPEN"' >/dev/null 2>&1; then
            EXISTING_PR=$(gh pr view develop --json number,body)
            echo "existing_pr_number=$(echo "$EXISTING_PR" | jq -r '.number')" >> $GITHUB_OUTPUT
            # 既存の body を保存(改行を保持)
            {
              echo "existing_body<<EOF"
              echo "$EXISTING_PR" | jq -r '.body'
              echo "EOF"
            } >> $GITHUB_OUTPUT
            echo "Found existing PR: $(echo "$EXISTING_PR" | jq -r '.number')"
          else
            echo "No existing open PR found from develop to main"
          fi

      - name: Get latest merged PR
        id: latest_pr
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # 直前にマージされた PR の情報を取得(トリガーとなった PR)
          # push イベントから直接取得できないため、最新のマージ済み PR を取得
          LATEST_PR=$(gh pr list \
            --base develop \
            --state merged \
            --json number,title,mergedAt \
            --jq 'sort_by(.mergedAt) | reverse | .[0] // empty')

          if [ -n "$LATEST_PR" ]; then
            PR_NUMBER=$(echo "$LATEST_PR" | jq -r '.number')
            PR_TITLE=$(echo "$LATEST_PR" | jq -r '.title')
            echo "pr_number=${PR_NUMBER}" >> $GITHUB_OUTPUT
            echo "pr_entry=- [ ] #${PR_NUMBER} ${PR_TITLE}" >> $GITHUB_OUTPUT
          fi

      - name: Create or Update Release PR
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          EXISTING_PR_NUMBER: ${{ steps.check.outputs.existing_pr_number }}
          EXISTING_BODY: ${{ steps.check.outputs.existing_body }}
          NEW_PR_ENTRY: ${{ steps.latest_pr.outputs.pr_entry }}
          MERGED_PR_NUMBER: ${{ steps.latest_pr.outputs.pr_number }}
        run: |
          if [ -n "$EXISTING_PR_NUMBER" ]; then
            # 既存の PR がある場合

            # 重複チェック: 同じ PR 番号が既に含まれているか確認
            if echo "$EXISTING_BODY" | grep -q "#${MERGED_PR_NUMBER} "; then
              echo "PR #${MERGED_PR_NUMBER} is already in the release PR body. Skipping."
              exit 0
            fi

            # 新しい PR エントリを末尾に追記
            UPDATED_BODY="${EXISTING_BODY}
          ${NEW_PR_ENTRY}"

            gh pr edit "$EXISTING_PR_NUMBER" --body "$UPDATED_BODY"
            echo "Updated existing PR #$EXISTING_PR_NUMBER with new entry: #${MERGED_PR_NUMBER}"
          else
            # 新規 PR を作成(REST API を使用)
            PR_BODY="## リリース内容

          以下の変更が含まれています。

          ${NEW_PR_ENTRY}"

            # REST API で PR を作成(GraphQL の権限問題を回避)
            gh api repos/${{ github.repository }}/pulls \
              --method POST \
              -f title="[YYYYMMDD]リリース" \
              -f body="$PR_BODY" \
              -f head="develop" \
              -f base="main"
            echo "Created new Release PR"
          fi

ポイント:

  • 新規作成時: タイトルは「[YYYYMMDD]リリース」。人間が YYYYMMDD を実際の日付に変更する
  • 既存 PR がある場合: PR の説明(body)に新しい PR エントリを追記するのみ。

ステップ 3: main マージ時のタグ付けワークフロー

.github/workflows/tag-release.yml を作成します:

name: Tag Release

on:
  pull_request:
    branches:
      - main
    types:
      - closed

permissions:
  contents: write

jobs:
  tag-release:
    # マージされた場合のみ実行
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0  # すべてのタグを取得するため

      - name: Extract date from PR title
        id: extract
        run: |
          # PR タイトルから日付を抽出(例: [20241225]リリース → 20241225)
          PR_TITLE="${{ github.event.pull_request.title }}"
          RELEASE_DATE=$(echo "$PR_TITLE" | grep -oP '\[\K\d{8}(?=\])')

          if [ -z "$RELEASE_DATE" ]; then
            echo "No date found in PR title, using today's date"
            RELEASE_DATE=$(date +%Y%m%d)
          fi

          echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT

      - name: Determine next tag number
        id: next_tag
        env:
          RELEASE_DATE: ${{ steps.extract.outputs.release_date }}
        run: |
          # リモートのタグを取得
          git fetch --tags

          # 同じ日付のタグを検索し、最大の連番を取得
          # 例: 20241225_01, 20241225_02 → 最大は 02
          EXISTING_TAGS=$(git tag --list "${RELEASE_DATE}_*" | sort -V)

          if [ -z "$EXISTING_TAGS" ]; then
            # 同じ日付のタグがない場合は _01 から開始
            NEXT_NUMBER="01"
          else
            # 最大の連番を取得して +1
            LAST_TAG=$(echo "$EXISTING_TAGS" | tail -1)
            LAST_NUMBER=$(echo "$LAST_TAG" | grep -oP '_\K\d+$')
            NEXT_NUMBER=$(printf "%02d" $((10#$LAST_NUMBER + 1)))
          fi

          TAG_NAME="${RELEASE_DATE}_${NEXT_NUMBER}"
          echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT

      - name: Create and push tag
        env:
          TAG_NAME: ${{ steps.next_tag.outputs.tag_name }}
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

          git tag -a "$TAG_NAME" -m "Release $TAG_NAME"
          git push origin "$TAG_NAME"

          echo "Created and pushed tag: $TAG_NAME"

ポイント:

  • 連番形式: タグは YYYYMMDD_01, YYYYMMDD_02, ... の形式で作成される
  • 同日複数回対応: 同じ日に複数回リリースしても、連番が自動的にインクリメントされる
  • 2桁ゼロ埋め: 連番は 01, 02, ... のように 2 桁でゼロ埋めされる

ワークフロー図

トラブルシューティング

PR が自動生成されない

  1. 権限の確認: permissionspull-requests: write が設定されているか確認
  2. ブランチの確認: develop と main の両方が存在するか確認
  3. 差分の確認: develop と main に差分があるか確認
git log main..develop --oneline

タグが作成されない

  1. 権限の確認: permissionscontents: write が設定されているか確認
  2. PR タイトルの確認: [YYYYMMDD]リリース 形式になっているか確認
  3. fetch-depth の確認: actions/checkoutfetch-depth: 0 が設定されているか確認
# 同じ日付のタグを確認
git tag --list | grep "$(date +%Y%m%d)"

# 例: 20241225_01, 20241225_02 などが表示される

マージ済み PR が取得できない

GitHub API の制限により、古い PR は取得できない場合があります。--limit オプションで取得数を調整:

gh pr list --base develop --state merged --limit 100

PR は作成されるがワークフローが失敗する

Resource not accessible by integration (createPullRequest.pullRequest) エラーが発生する場合、gh pr create コマンドの GraphQL API 呼び出しで問題が発生しています。

これは GitHub CLI の既知の問題で、PR 作成後に追加情報を GraphQL で取得しようとする際に、GITHUB_TOKEN では取得できない情報にアクセスしようとするためです。PR 自体は作成されますが、コマンドはエラーで終了します。

解決策: REST API を使用して PR を作成します。

# gh pr create の代わりに REST API を使用
gh api repos/${{ github.repository }}/pulls \
  --method POST \
  -f title="[YYYYMMDD]リリース" \
  -f body="$PR_BODY" \
  -f head="develop" \
  -f base="main"

REST API は GraphQL API と異なり、追加情報の取得を行わないため、この権限問題を回避できます。

まとめ

このワークフローにより:

  1. 手動作業の削減: develop へのマージだけで、リリース PR が自動生成/更新される
  2. リリース内容の可視化: チェックリスト形式で含まれる変更が一目でわかる
  3. 柔軟なリリース日設定: PR タイトルの日付は人間が自由に設定できる
  4. 同日複数回リリース対応: YYYYMMDD_01, YYYYMMDD_02 形式で連番管理

自動化される部分と手動の部分

項目自動/手動説明
develop → main PR の作成自動「[YYYYMMDD]リリース」タイトルで作成
PR 本文への追記自動新しい PR がマージされるたびに追記
PR タイトルの日付設定手動YYYYMMDD を実際の日付に変更
タグの連番付与自動YYYYMMDD_XX 形式で自動インクリメント

GitHub CLI を使ったカスタム実装により、プロジェクト固有の要件に完全に対応したワークフローを構築できます。