git-flow (not GitHub-Flow) はgitにおけるブランチ運用のプラクティスのひとつです。
git-flowによるリリースまでの手順は定型作業のためできるだけ自動化を進めたいところですが、アウェアファイでは半自動でワークフローが運用されていました(います)。元々半自動化状態になっていたのは完全自動化の仕組みを簡単につくる時間をとれていなかったということなのですが、改めて調べてみても、そのまま利用できるGitHub Actionsや流用できるGitHub Actionsの例は見当たりませんでした。
そこであらたにゼロベースでGitHub Actionsをつくってみることにしました、というのが本稿の趣旨となります。
git-flowをGitHub Actionsに任せるまでのステップ
git-flowによるリリースプロセスの自動化において実現したいステップとしては次のようなものがあります。
- 最新の
develop
ブランチからrelease/x.x.x
ブランチを作成する release/x.x.x
ブランチからmain
ブランチにpull-requestを作成する- pull-requestのタイトルにリリースバージョンを、概要として今回のリリースに含まれる
develop
ブランチにマージ済みのpull-request差分の情報を記載する - pull-requestを(GitHub 上で)マージすると、マージコミットへのバージョニング(タグ付け)、GitHub Releaseの作成を行う
- pull-requestをマージすると、
release/x.x.x
ブランチからdevelop
ブランチへのマージ(バックマージ)を行う
git-flowにのっとって進めるだけではあるのですが、GitHub上のリソースを作成したり、後述するブランチ保護との兼ね合いなど、いくつかの考慮事項があります。
なお、リリース作業そのものについてはデプロイ先に依存するため本稿では取り扱いません。main
ブランチにタグが付けられたことをトリガーにリリースプロセスが開始される、といった既存のワークフローがあることを想定しています。
ブランチ保護ルールについて
git-flowに限らない話しですが、GitHub Actions上でブランチに対する操作を自動化しようとすると、ブランチ保護ルールとの競合が問題になります。
ブランチ保護ルールを利用してmainブランチやdevelopブランチへの直接pushを禁止していたり、マージできる条件にpull-requestでacceptされているかの条件を付与しているチームは多いのではないかと思います。こうしたブランチ保護ルールがあると、GitHub Actions上でdevelopブランチにpushする、ブランチを自動でマージするといったことが行えない状態になります。
この対応策としてGitHub Appを作成してバイパスリストに加える、ということが2025年1月時点だと唯一できる妥当な選択肢のようでしたので、この方法を検討することをお勧めします。
参考 :
調べるとPersonal access tokens(PAT)を利用する選択肢がでてくるのですが、PATは個人アカウントに紐付いてしまうデメリットがあります。また手元で検証した限り、現在GitHubが推奨しているあたらしいPAT形式であるFine-grained PATでは、ブランチ保護ルールをバイパスできなかったこともありPATの採用は見送りました。
git-flow自動化のためのGitHub Actions
今回作成したGitHub Actionsを紹介します。
※ エラーハンドリングやブランチ・コミットの状況への依存がどこまであるか検証が足りない部分もあるかと思いますので、参考にされる場合はその点ご留意ください。
name: Git-Flow Release Workflow
# Trigger workflow either manually with version input or when a PR is closed
on:
workflow_dispatch:
inputs:
version:
description: 'Specify the release version (e.g., 1.0.0)'
required: true
pull_request:
types: [closed]
branches:
- main
permissions:
contents: write
pull-requests: write
jobs:
# Job to create a release PR from develop to main
create-release-pr:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for proper git operations
# Ensure we're working with the latest develop branch
- name: Ensure develop is up to date
run: |
git fetch origin
git checkout develop
git pull origin develop
# Get list of PRs merged since the last release
- name: Get unreleased PRs
id: get-prs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Find the last tag, if any exists
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
# Build search query based on last release date
SEARCH_QUERY=""
if [ -n "$LAST_TAG" ]; then
SEARCH_QUERY="merged:>$(git log -1 --format=%aI $LAST_TAG)"
fi
# Generate markdown list of PRs for release notes
echo "Generating PR list since $LAST_TAG ..."
gh pr list \
--base develop \
--state merged \
--json title,number,url,author,mergedAt \
--search "$SEARCH_QUERY" \
| jq -r '.[] | "- [#\(.number) \(.title)](\(.url)) by @\(.author.login)"' > pr_list.md
# Create or checkout existing release branch
- name: Checkout or create release branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION=${{ github.event.inputs.version }}
BRANCH="release/$VERSION"
# Check if release branch already exists
if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
echo "Branch $BRANCH already exists. Checking out the existing branch."
git checkout -B "$BRANCH" "origin/$BRANCH"
else
echo "Branch $BRANCH does not exist. Creating a new branch from develop."
git checkout -b "$BRANCH" develop
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
# Push both the branch and the version update
git push --set-upstream origin "$BRANCH"
fi
# Create new PR or update existing one
- name: Create or update PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION=${{ github.event.inputs.version }}
BRANCH="release/$VERSION"
# Check for existing PR
EXISTING_PR=$(gh pr view --base main --head "$BRANCH" --json number --jq '.number' 2>/dev/null || true)
if [ -n "$EXISTING_PR" ]; then
echo "PR #$EXISTING_PR already exists. Updating..."
gh pr edit "$EXISTING_PR" \
--title "Release v$VERSION" \
--body "$(cat pr_list.md)"
else
echo "No existing PR found. Creating a new one."
gh pr create \
--base main \
--head "$BRANCH" \
--title "Release v$VERSION" \
--body "$(cat pr_list.md)"
fi
# Post-merge job to create release and sync develop
post-merge:
if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main'
runs-on: ubuntu-24.04
steps:
# Generate GitHub App token for authentication
- name: Generate Token
id: generate-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.GITHUB_APP_ID }}
private-key: ${{ secrets.GITHUB_APP_ID_PRIVATE_KEY }}
# Checkout repository with the generated token
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: true
token: ${{ steps.generate-token.outputs.token }}
# Create release, tag, and sync develop branch
- name: Extract version and create release
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
run: |
# Extract version from PR title
VERSION=$(echo "${{ github.event.pull_request.title }}" | grep -oP 'v\d+\.\d+\.\d+')
MERGE_SHA=${{ github.event.pull_request.merge_commit_sha }}
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
# Create and push tag
git tag $VERSION $MERGE_SHA
git push origin $VERSION
# Create GitHub release
echo "${{ github.event.pull_request.body }}" > release_notes.md
gh release create $VERSION \
--title "Release $VERSION" \
--generate-notes \
--notes-file release_notes.md \
--target $MERGE_SHA
# Sync develop branch with the release
git checkout develop
git checkout -b release/${VERSION#v} origin/release/${VERSION#v}
git switch develop
git merge --no-ff release/${VERSION#v}
git push origin develop
# Delete release branch
git branch -d release/${VERSION#v}
git push origin --delete release/${VERSION#v}
このGitHub Actionsは2つの機能を提供します。
workflow_dispatch
を利用して、開発者が指定したバージョンにもとづいてgit-flowプロセスを開始、pull-requestを作成する機能main
ブランチへのマージをトリガーとし、タグ付けやバックマージなどの航続処理を行う機能
サードパーティのGitHub Actionsを利用していないため少しステップが長いですが、各処理自体は粛々と行っているだけなので、読み解いてみてください。(にしては完成するのに時間を溶かしましたが...)
工夫がある点としては、develop
ブランチにマージされておりmainブランチにはマージされていないpull-requestの履歴をリストアップしてくれる点や、同名のブランチや、pull-requestがすでに作成されている場合は再利用するようにしている点でしょうか。
前述のとおり、あらかじめGitHub Appを作成し適切なパーミッションを与え、GitHub AppのIDとトークンを入手しておく必要があります。GitHub Appの設定の詳細については今回割愛いたします。
このワークフローは開発者が明示的にバージョンを指定し、開始できます。
作成されるpull-requestは次のようになります。
掲載したスクリーンショットはマージ済みのものですが、pull-requestをマージすると、タグ付けやReleaseの作成が行われます。
今回作成したGitHub Actionsには残課題があるため備忘録として以下に記します。
- hotfixリリースに対応していないこと(baseとheadブランチを調整すればすぐできそうではあります)
pull-requestの概要欄テキスト作成処理のエスケープ処理が不充分で、マージ済みのpull-requestに「`」が利用されていたりすると処理が失敗すること- => 内容をファイルに書き出し
--notes-file
オプションを指定することで対応できました
- => 内容をファイルに書き出し
以上です。ぜひ、感想や改善点があれば @iktakahiro までお気軽にコメントください。
Discussion