こんにちはKenzです。
今回は、あるリポジトリのPull requestがマージされたときに、異なるリポジトリにその変更を反映するPull requestを作るGithub Actionsのワークフローを作る方法を紹介します。
複数リポジトリの連携を自動化したい
GitHubで他のリポジトリに依存したリポジトリを作ることがありませんか?
例えば複数プロジェクトで共通利用しているライブラリのリポジトリとそのライブラリを使うアプリというような場合です。
そのような場合、ライブラリ側のリポジトリに変更があった時、それを使うアプリのリポジトリでライブラリの変更を取り込み、ビルドをやり直したPull requestを作る必要があります。
この作業は定型的になりがちなので自動化したいところです。
今回はリポジトリの変更を検知して、異なるリポジトリへPull request作成を自動化するGitHub Actionsのワークフローを作る方法を紹介します。
説明を簡単にするため、依存される側のリポジトリをライブラリのリポジトリ、依存するリポジトリをアプリのリポジトリと呼ぶことにします。
もちろん、アプリとライブラリに限らずVMのイメージやMLのモデルとそれを使うKubernetesなど、様々な場面で使用することができます。
ライブラリリポジトリの実装
Pull requestを作る条件
最も簡単にPull requestを作成することを考えるなら、ライブラリリポジトリのPull requestがマージされたタイミングで無条件にアプリリポジトリのPull requestを作成するだけです。
ところがそれだとライブラリの README.md を更新するだけでアプリのリポジトリにPull requestが作成されてしまいノイズとなってしまいます。
そこで、ライブラリリポジトリで特定のファイルに変更が加わったときのみ、Pull requestを作成するようにします。
アプリリポジトリのPull requestはアプリリポジトリで作る
アプリリポジトリのPull requestをライブラリリポジトリから直接作成することもできますが、そうするとライブラリのワークフローにアプリのリポジトリでPull requestのWrite権限を与える必要が出てしまいます。
ライブラリが複数のアプリで使用されている場合、ライブラリのPull requestが使用するリポジトリに対して過剰な権限を持つことになりあまり望ましくないです。
そこで、ライブラリのリポジトリからはアプリのワークフローを呼ぶだけとし、アプリリポジトリのPull requestはアプリリポジトリ自体のワークフローが作成するようにします。
ライブラリリポジトリの差分を取る
アプリのリポジトリでPull requetを作る必要があるかどうかを判断するために、ライブラリのリポジトリで行われたPull requestでの変更点を洗い出します。
Create a merge commitあるいはSquasy and mergeを使用した場合、
git diff HEAD^ --name-only
を行うことでマージコミットを見て変更内容の一覧を簡単に得ることができます。
特定フォルダ内の変更のみに限定する場合は
files=(`git diff HEAD^ --name-only --relative=<フォルダ名>|tr '\n' ' ' `)`
のようにrelativeスイッチを使用します。
name: library_update
on:
pull_request:
branches:
- "main"
types: [closed]
jobs:
check_after_merge:
if: github.event.pull_request.merged == true
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
name: checkout
with:
fetch-depth: 2
- name: check_diff
id: check_diff
shell: bash
run: |
files=(`git diff HEAD^ --name-only --relative=src|tr '\n' ' ' `)
echo "changed_files=${files%,*}" >> $GITHUB_OUTPUT
アプリリポジトリのワークフローを呼び出す
該当のファイルに変更があった場合、アプリリポジトリのPull requestを作成します。
上記で説明した通り、ライブラリリポジトリのワークフローで直接アプリリポジトリのPull requestを作成するのではなく、ライブラリリポジトリのワークフローはアプリリポジトリのAPIを呼び、Pull requestの作成はアプリリポジトリのワークフローが行います。
アプリリポジトリへアクセス可能なトークンの作成
アプリリポジトリAPIを呼び出せるように、まずはトークンを作成します。
アクセストークンには Fine-grained personal access tokenとPersonal access tokens (classic)がありますが、今回はよりセキュリティレベルの高いFine-grained personal access tokenを使用します
https://github.com/settings/tokens を開いて、Fine-grained personal access tokensを選び、Generate new token を選びます。
Token nameに識別可能な名前を設定
Repository accessにOnly select repositoriesを選択しアプリポジトリを選びます。
PermissionsのContentsにRead and Writeの権限を付与してGenerate tokenをクリックするとトークンが表示されるのでコピーします。
次にライブラリリポジトリのSettingsを開き、Secrets and variablesのActionsを選びます
New repository secretsを選び、Nameに CALL_DISPACHER Secretに先ほど表示されたトークンを貼り付けます。
このトークンを使用してライブラリのGithub ActionsからアプリのGithub Actionsを実行します。
アプリリポジトリのAPIをPOSTする
アプリのGithub Actionsを呼ぶには先ほど作成したトークンを使用して、 https://api.github.com/repos/<アプリリポジトリのオーナー>/<アプリリポジトリ>/dispatches をPostします。
アプリリポジトリが https://github.com/kenz/sample_app_repository の場合は次のようになります。
curl \
-X POST \
-H "Authorization: token ${{ secrets.CALL_DISPACHER }}" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/kenz/sample_app_repository/dispatches \
-d '{"event_type":"update-library","client_payload":{"date": ”today" }}'
event_typeにはこれからアプリリポジトリに作成するワークフローの名前を設定します。
client_payloadには追加で送りたい値を設定します。
例えば、srcフォルダ内のファイルが変更された場合に、変更されたファイルの一覧を送りたい場合、ワークフローは次のようになります。
name: library_update
on:
pull_request:
branches:
- "main"
types: [closed]
jobs:
check_after_merge:
if: github.event.pull_request.merged == true
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
name: checkout
with:
fetch-depth: 2
- name: check_diff
id: check_diff
shell: bash
run: |
files=(`git diff HEAD^ --name-only --relative=src|tr '\n' ' ' `)
echo "changed_files=${files%,*}" >> $GITHUB_OUTPUT
- name: call_app
shell: bash
if: ${{ steps.check_diff.outputs.changed_files != '' }}
run: |
curl \
-X POST \
-H "Authorization: token ${{ secrets.CALL_DISPACHER }}" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/kenz/sample_app_repository/dispatches \
-d '{"event_type":"update-library","client_payload":{"change_files": ”${{ steps.check_diff.outputs.changed_files }}", "pull_request_url": "${{ github.event.pull_request.html_url }}" }}'
このyamlファイルをライブラリのリポジトリ内 .github/workflowsディレクトリ配下に保存します。
これで送信側のワークフローは完成です。
アプリリポジトリ側のワークフローを作る。
つぎに、送られたリクエストを元にPull reuqestを作成するワークフローをアプリのリポジトリに作成します。
アプリリポジトリで使用するトークンを追加
まずはライブラリリポジトリ同様にPull requestを作るトークンを作成します。
https://github.com/settings/tokens を開いてFine-grained personal access tokensを選び、Generate new token を選びます。
Token nameに識別可能な名前を設定
Repository accessにOnly select repositoriesを選択しアプリポジトリを選びます。
PermissionsのContentsと Pull requests にRead and Writeの権限を付与してGenerate tokenをクリックするとトークンが表示されるのでコピーします。
アプリリポジトリのSettingsを開き、Secrets and variablesのActionsを選びます
New repository secretsを選び、Nameに CREATE_PULL_REQUEST_TOKEN Secretに先ほど表示されたトークンを貼り付けます。
このトークンを使用してアプリリポジトリにPull requestを作成します。
Pull requestを作るワークフロー
次にPull requestを作成するワークフローをアプリリポジトリの.github/workflows内に作ります。
実行条件と入力値
このワークフローはライブラリリポジトリからdisplach APIが呼ばれたときに実行するため、実行条件は repository_displach とし types にはevent_typeで指定したのと同じ名前を指定します。
Pull requestを作成するためにpermissionとしてcontentsとpull-requests、id-tokenのwrite権限を指定します。
ライブラリリポジトリから送られてくるchange_filesやpull_request_urlなどの追加情報は
${{ github.event.client_payload.change_files}} のように指定することで取得することができます。
ここでは移行性をよくするためにenvへ移し替えています。
name: Create PR of Update library
on:
repository_dispatch:
types: [update-library]
permissions:
contents: 'write'
pull-requests: 'write'
id-token: 'write'
env:
CHANGE_FILE: ${{ github.event.client_payload.change_files}}
PULL_REQUEST_URL : ${{ github.event.client_payload.pull_request_url }}
GIT_PR_BASE_BRANCH: main
GIT_PR_BRANCH: feature/library-update
ブランチの作成
ブランチを作ります
ブランチはgitコマンドでローカルで実行するときと同じように作成します。
git configでユーザー名やメールアドレスを指定します。
jobs:
create-branch:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
ref: main
- name: create-branch
run: |
git remote set-url origin https://github-actions:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}
git config --global user.name "auto-pull-request-creater"
git config --global user.email "foo@example.com"
git pull origin ${{ env.GIT_PR_BRANCH }} 2>/dev/null || git checkout -b ${{ env.GIT_PR_BRANCH }}
git push -f origin HEAD:${{ env.GIT_PR_BRANCH }};
今回ブランチ名は説明を省くためにfeature/library-updateと固定にしていますが 、複数のライブラリなどから汎用的に使えるようにするにはブランチ名もclient_payloadで渡すなどして切り分けたほうが良いかと思います。
コミットを切る
ライブラリの変更を反映するコミットを切ります。
更新されたライブラリをどのように取り扱うかはそれぞれのプロジェクトによって変わるためここでは触れずに、単に渡されたファイル名をlibrary.txtに追記していくだけとしています。
各自のプロジェクトに適合するように書き換えてください。
- name: create-commit
run: |
printf "${{ env.CHANGE_FILE }}" >> library.txt
git add library.txt
git commit -m "Update library"
git push origin ${{ env.GIT_PR_BRANCH }}
Pull requestの作成
最後にPull requestを作成します。
環境変数GH_TOKENにSecretに格納したトークンを渡すのを忘れないようにしてください。
タイトルは決め打ち、本文はライブラリリポジトリから渡されたPullRequestのURLを表示するようにしています。
この部分についても必要に応じて書き換えてください。
オプションでレビューアーを自動で付与するなどの設定も可能です。
- name: Create a pull request
env:
GH_TOKEN: ${{ secrets.CREATE_PULL_REQUEST_TOKEN}}
run: |
already_pr=(\
$( gh pr list --base ${{ env.GIT_PR_BASE_BRANCH }} \
--head ${{ env.GIT_PR_BRANCH }} \
--state open \
--base main \
--json number | jq '. | length'))
if [ $already_pr = 0 ] ; then
gh pr create --base ${{ env.GIT_PR_BASE_BRANCH }} \
--head ${{ env.GIT_PR_BRANCH }} \
--body "created by ${{ env.PULL_REQUEST_URL }} " \
--title "Update library"
else
printf "already exist pull request"
fi
アプリリポジトリのワークフロー
ワークフロー全体は次のようになります。
name: Create PR of Update library
on:
repository_dispatch:
types: [update-library]
permissions:
contents: 'write'
pull-requests: 'write'
id-token: 'write'
env:
CHANGE_FILE: ${{ github.event.client_payload.change_files}}
PULL_REQUEST_URL : ${{ github.event.client_payload.pull_request_url }}
GIT_PR_BASE_BRANCH: main
GIT_PR_BRANCH: feature/library-update
jobs:
create-branch:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
ref: main
- name: create-branch
run: |
git remote set-url origin https://github-actions:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}
git config --global user.name "auto-pull-request-creater"
git config --global user.email "foo@example.com"
git pull origin ${{ env.GIT_PR_BRANCH }} 2>/dev/null || git checkout -b ${{ env.GIT_PR_BRANCH }}
git push -f origin HEAD:${{ env.GIT_PR_BRANCH }};
- name: create-commit
run: |
printf "${{ env.CHANGE_FILE }}" >> library.txt
git add library.txt
git commit -m "Update library"
git push origin ${{ env.GIT_PR_BRANCH }}
- name: Create a pull request
env:
GH_TOKEN: ${{ secrets.CREATE_PULL_REQUEST_TOKEN}}
run: |
already_pr=(\
$( gh pr list --base ${{ env.GIT_PR_BASE_BRANCH }} \
--head ${{ env.GIT_PR_BRANCH }} \
--state open \
--base main \
--json number | jq '. | length'))
if [ $already_pr = 0 ] ; then
gh pr create --base ${{ env.GIT_PR_BASE_BRANCH }} \
--head ${{ env.GIT_PR_BRANCH }} \
--body "created by ${{ env.PULL_REQUEST_URL }} " \
--title "Update library"
else
printf "already exist pull request"
fi
ということで、今回はPull requestを送る方法を紹介しました。
地味だけれど、大量に似たようなPull requestを作っている場合などに思い出してください。