GitHub ActionsのCustom Deployment Protection Ruleを理解する
GitHub Actions Meetup Tokyo #4で発表があった以下のスライドと、そこで紹介されていたブログより
発表の中ではGitHub Actions -> Azureに非同期処理を投げて、その完了をGitHub Actions側で今までポーリングしていたのをCustom Deployment Protection Ruleを使ってポーリング無しにできたので費用を圧縮できましたという話が面白かった。
業務でGitHub Actionsで別のワークフローが終わるまでポーリングしているコードを見たことがあるので、そういうのを置き換える方法として知っておきたいので試してみる。
そもそもなぜポーリング処理をなくしたいかというと、シンプルにCIにかかるお金を減らしたいため。
GitHub Actionsをヘビーに使って無料枠の範囲を超えてくると意外に馬鹿にならないお値段になってくる。そうなった場合にちまちまとCIのチューニングをしている横でただのポーリング処理のために10分-20分の料金を支払っていると悲しい気持ちになる。
Custom Deployment Protection Ruleは全てのリポジトリで使えるわけではなく、PublicかEnterpriseプランじゃないと使えない。
けど自分のPublicリポジトリでenvironmentの設定画面を見てもこのようになっていて、有効にするためのUIが出てきていなかった。
ドキュメントによるとDatadogやHoneycombなどが出しているGitHub Appか、自分でその機能を持ったGitHub Appを作ってリポジトリにインストールする必要があると書いてあるので、そもそもインストールしないとUIが変わらないのかもしれない
適当なGitHub Appを作成。Appに付けるパーミッションはドキュメントの通り
注意点として、"Subscribe to events"の項目はWebhookをActiveにしてWebhook URLを入れないとそもそも出てこないので設定ができない。このwebhookが必要か不要かはあまり分かっていないが、webhookの中身も見てみたいのでとりあえずactiveにしておく。
webhookの送り先は人様に迷惑をかけないようにこの後実験で使う https://github.com/Kesin11/actions-newfeature-playground にしておく(githubには申し訳ないけど)
作ったGitHub Appを実験リポジトリにインストールしてから再度Environmentの設定画面を見てみる。
たしかにさっき作ったGitHub Appが選択可能になっている。
とりあえずapprove待ち状態になるのを見てみたいので最小限の機能だけのワークフローを作ってみる
name: Custom Deployment Protection Rule
on:
push:
branches:
- custom_deploy_protection
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
# Wait for approve from GitHub App
environment: custom-protection
steps:
- uses: actions/checkout@v4
- run: echo "Deploy to custom-protection"
これをトリガーすると、このように kesin11-custom-deployment-protection
のGitHub Appによるレビュー待ち状態となりジョブが始まる前の状態で待機させられている。
ちなみにここで見えている Start all waiting jobs
ボタンを押すと、Admin権限でGitHub Appに成り代わってapproveを出すことが可能っぽい
このCustom Deployment Protection Ruleによるレビュー待ちを実際にGitHub Appから承認させてみる。
まずはGitHub Actions内から承認のAPIを叩けるのかだけを確かめたいので、超シンプルに単一のワークフロー内にレビュー待ちと承認の2つの機能を押し込める。
- pushトリガー:承認待ち状態を作る
- workflow_dispatchトリガー:inputにrun_idを渡して実行し、GitHub Appに成り代わってトークンを生成して
/repos/OWNER/REPO/actions/runs/RUN_ID/deployment_protection_rule
のAPIを叩いて承認する
承認のためのAPIのドキュメントはこのあたり
これをワークフローのコードに落とし込むとこんな感じ
name: Custom Deployment Protection Rule
on:
push:
branches:
- custom_deploy_protection
workflow_dispatch:
inputs:
approve_run_id:
description: 'run_id to approve'
required: true
jobs:
deploy:
runs-on: ubuntu-latest
# Wait for approve from GitHub App when trigger by push
environment: custom-protection
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- run: echo "Deploy to custom-protection"
approve:
runs-on: ubuntu-latest
# Approve to awaiting run_id when trigger by workflow_dispatch
if: github.event_name == 'workflow_dispatch'
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.DEPLOYMENT_APP_ID }}
private-key: ${{ secrets.DEPLOYMENT_APP_PRIVATE_KEY }}
- name: Approve Deployment
run: |
gh api -X POST /repos/${{ github.repository }}/actions/runs/${{ github.event.inputs.approve_run_id }}/deployment_protection_rule \
-f "environment_name=custom-protection" \
-f "state=approved" \
-f "comment=Approved by workflow_dispatch"
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
pushすると deploy
ジョブの方が承認待ち状態となるので、そのときのrun_idをURLからコピーする。次にworkflow_dispatchで起動するときにそのrun_idを渡して起動する。
pushトリガーで承認待ちとなったジョブ:https://github.com/Kesin11/actions-newfeature-playground/actions/runs/10575173774
workflow_dispatchトリガーで承認したジョブ:https://github.com/Kesin11/actions-newfeature-playground/actions/runs/10575184008
無事にpushトリガーのジョブの方が承認されて動き出した様子。
ここまでで確認できたこと
- 自作のGitHub AppでCustom Deployment Protection Ruleの機能はちゃんと動作する
- GitHub Appのwebhookは必須ではない(ここまでの使い方であれば)
- acitons/create-github-app-tokenでGitHub Appのトークンを得れば承認のAPIをGitHub Actions内から叩くことは可能
- つまり、異なるワークフロー同士で承認待ち状態と承認動作をGitHub Actions内で完結可能
もう少し実践的なパターンを考えてみる。
一般的なビルド->デプロイの流れは、基本的に単一のワークフロー内で別々のジョブとしておき needs
で依存関係を作る方法が簡単で一般的。
しかし、ビルドとデプロイのそれぞれの処理が複雑になってくると単一のワークフロー内で書くのが大変になってきたり、ビルド処理は使いまわしたいなどの理由で2つのワークフローに分離されるケースもある。
ありがちな例としてはこういうケースだが、要は同じコミットハッシュでトリガーされた2つのワークフローの間にA->Bの順番の依存関係を作りたいという場合がある。
順を追って流れを書くと、
- pushされたときにAとBを同時に起動
- BはAより先に承認待ち状態になる
- Aは時間がかかる処理を行い、その後に承認待ちのBを見つけたらApproveする
- BはAからのApproveを受けて起動する
2つのワークフローでこれを実現するコード
デプロイジョブB(サンプルとして承認待ちするだけなので、先程のコードをほぼ再掲)
name: Custom Deployment Protection Rule
on:
push:
branches:
- custom_deploy_protection
workflow_dispatch:
inputs:
approve_run_id:
description: 'run_id to approve'
required: true
jobs:
deploy:
runs-on: ubuntu-latest
# Wait for approve from GitHub App when trigger by push
environment: custom-protection
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- run: echo "Deploy to custom-protection"
ビルドジョブA(Bより時間がかかる処理を行い、その後Bをapproveする)
name: Auto Approve Custom Deployment
on:
push:
branches:
- custom_deploy_protection
jobs:
build_dummy:
runs-on: ubuntu-latest
# 時間がかかるジョブを再現するためにsleepするだけ
steps:
- uses: actions/checkout@v4
- run: sleep 10
auto_approve:
runs-on: ubuntu-latest
# 同じコミットハッシュからトリガーされ、承認待ち状態の別のワークフローのrun_idを特定して保存する
needs: build_dummy
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.DEPLOYMENT_APP_ID }}
private-key: ${{ secrets.DEPLOYMENT_APP_PRIVATE_KEY }}
- name: Find awaiting approval run_id
id: awaiting-run
run: |
RUN_ID=$(gh api /repos/${{ github.repository }}/actions/workflows/custom_deploy_protection.yml/runs \
-X GET -f head_sha=${{ github.sha }} |\
jq -e 'first(.workflow_runs[].id)')
echo "run_id=${RUN_ID}" >> $GITHUB_OUTPUT
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# 本当は gh api /repos/${{ github.repository }}/actions/runs/${RUN_ID}/pending_deployments を見て承認待ち状態かどうか判別するべきだが、
# 今回は簡略化のために必ず承認待ち状態であると仮定してapproveする
# 先ほど保存したrun_idに対してapproveを行う
- name: Approve Deployment
run: |
gh api -X POST /repos/${{ github.repository }}/actions/runs/${{ steps.awaiting-run.outputs.run_id }}/deployment_protection_rule \
-f "environment_name=custom-protection" \
-f "state=approved" \
-f "comment=Approved by workflow_dispatch"
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
ポイントは同じコミットハッシュから起動された特定のワークフローのrun_idを調べるために実行しているこのAPI。
今回のサンプルはBのワークフローだけにapproveを出すのでBのyamlを指定している。もしもさらに複数のワークフローがAの処理待ちを待つような複雑なケースだともう少し工夫が必要になりそう。
gh api /repos/${{ github.repository }}/actions/workflows/custom_deploy_protection.yml/runs \
-X GET -f head_sha=${{ github.sha }} |\
jq -e 'first(.workflow_runs[].id)`
まとめ
Custom Deployment Protection Ruleは元記事で紹介されていたように、GitHub Actions -> 別のクラウドの非同期処理 -> GitHub Actionsのような複雑な呼び出し順を実現するためのポーリング処理を置き換えることが可能だが、GitHub Actions内のワークフロー間の複雑な依存関係にも応用できる。
必要になるのはこの用途のための自作Github Appと、Privateリポジトリの場合はGitHub Enterpriseのライセンス。
今回はサンプルのための最低限のコードを意識したのでほぼghコマンドだけで完結できるようにしましたが、実際はもっと複雑になると思われるのでjsやgoなど別言語でGitHubのAPIを叩くスクリプトや、カスタムactionを作った方がよさそう。