GitHub Actions で Vercel bot みたいなプレビューデプロイをつくる
Vercel というウェブサイトをデプロイできるサービスがあって、それを GitHub と連携すると、
GitHub で push したりプルリクエストを開いたりしたときに、自動でいろいろやってくれるんですよ。
デフォルトブランチに push したら自動で再デプロイしてくれたり
デフォルト以外のブランチに push したら自動で preview version をデプロイしてくれて、
PR を開くと自動でコメントしてくれたり
再度 push すると再度プレビューをデプロイしてくれたりします。
特に PR 開くと自動でプレビューをつくってくれるのは、さくっと動作確認できてレビューにもすごく便利です。
Vercel を使っているときはこれを GitHub のリポジトリと連携するだけで、特に設定しなくてもこういう便利なことをやってくれるんですが、
使っていないときにもこれができたらなあと思ったわけです。
ということで、GitHub Actions でこのプレビューデプロイと似たようなことをやってみましょう!
そもそも vercel[bot] は何をやっているのか
どの GitHub のイベントがどういうふうにプレビューデプロイや PR コメントをトリガーしているのか観察してみると、
- push したときにデプロイがはじまる
- PR を開いたときにコメントする
- デプロイが終わっていなければ Preview: In Progress とコメントする
- デプロイが終わり次第コメントを Preview URL をのせたものに更新する
- デプロイがすでに終わっていれば Preview URL をすぐにコメントする
こんな感じかと思います。
PR を開いたときにはじめてデプロイをするという方法もあり、ワークフローがひとつで済むので簡単ですが、
push してから PR を開くまでの間にデプロイをすすめておいて、PR を開いたらすぐにコメントしてくれるほうが時短になるので、これを実現したいです。
処理の内容を図にすると次のようになります。
デプロイが終わるのと PR を開くのと、どちらが先に終わるかによって通るフローが変わってきます。
デプロイが先に終わっていれば PR が開かれたらすぐに preview URL をコメントするし、
PR が先に開いていればデプロイが完了するまではデプロイ中であるとコメントし、デプロイが完了すれば preview URL をのせたコメントに更新します。
ワークフローを書く
見たところワークフローが GitHub に対して必要になる処理は、
- PR にコメントを作成・更新する
- デプロイが完了しているか確認する
- 対応する PR が開いているか確認する
いずれも GitHub API を使うことで実現できます。
PR コメントに関しては peter-evans/find-comment
と peter-evans/create-or-update-comment
を使うのが簡単かと思います。
「デプロイが完了しているか確認する」に関しては、
GitHub の environment を作成して、デプロイ前に「デプロイ中」のステータスを付与し、デプロイ後に「完了」のステータスを付与することで、
GitHub API によってデプロイ状態を取得できます。
これは bobheadxi/deployments
を使うと簡単です。
今回のコード例ではこれらを使用します。
push したときの処理
push したときには、デプロイをし、デプロイ後にそのブランチに対応する PR が存在するかチェックし、存在していたらコメントを残します。
コード例は以下のようになります。
name: Deploy preview
on:
push:
branches: # branches other than main
- '*'
- '!main'
env:
ENV_NAME: preview-${{ github.ref_name }} # preview-[branch name]
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
permissions:
deployments: write # bobheadxi/deployments needs this
outputs:
url: ${{ steps.deploy-url.outputs.url }}
steps:
- name: Start deployment
uses: bobheadxi/deployments@v1
id: deployment
with:
step: start
token: ${{ github.token }}
env: ${{ env.ENV_NAME }}
# Authenticate and deploy steps...
- name: Deploy
id: deploy-url
run: # Output the deployment URL
- name: Update deployment status
uses: bobheadxi/deployments@v1
if: always()
with:
step: finish
token: ${{ github.token }}
status: ${{ job.status }}
env: ${{ steps.deployment.outputs.env }}
env_url: ${{ steps.deploy-url.outputs.url }}
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
check-if-pr-exists:
needs: deploy
name: Check if associated PR exists
runs-on: ubuntu-latest
permissions:
pull-requests: read
outputs:
pr-number: ${{ steps.get-pr-number.outputs.pr-number }}
steps:
- name: Get associated PRs
id: prs
run: >
echo "pulls=$(gh api -H "Accept: application/vnd.github.v3+json" -X GET
/repos/${{ github.repository }}/pulls
-f state=open
-f head=${{ github.repository_owner }}:${{ github.ref_name }})" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ github.token }}
- name: Get PR number
id: get-pr-number
run: |
if [ "${{ fromJSON(env.PULL) }}" ]; then
echo "pr-number=${{ fromJSON(env.PULL).number }}" >> $GITHUB_OUTPUT;
else
echo "pr-number=0" >> $GITHUB_OUTPUT;
fi
env:
PULL: ${{ toJSON(fromJSON(steps.prs.outputs.pulls)[0]) }}
comment:
needs:
- deploy
- check-if-pr-exists
name: Comment on PR
if: needs.check-if-pr-exists.outputs.pr-number != 0
runs-on: ubuntu-latest
permissions:
contents: read # actions/checkout needs this
issues: write # peter-evans/create-or-update-comment needs this
pull-requests: write # peter-evans/create-or-update-comment needs this
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Find Comment
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ needs.check-if-pr-exists.outputs.pr-number }}
comment-author: github-actions[bot]
body-includes: preview
- name: Get datetime for now
id: datetime
run: echo "value=$(date)" >> $GITHUB_OUTPUT
env:
TZ: Asia/Tokyo
- name: Create or update comment
uses: peter-evans/create-or-update-comment@v2
with:
issue-number: ${{ needs.check-if-pr-exists.outputs.pr-number }}
comment-id: ${{ steps.fc.outputs.comment-id }}
body: |
:eyes: Visit the **preview** for this PR (updated for commit ${{ github.sha }}):
[![View the preview deployment at ${{ needs.deploy.outputs.url }}](https://img.shields.io/badge/-View_Preview-eee)](${{ needs.deploy.outputs.url }})
<sub>(:clock3: updated at ${{ steps.datetime.outputs.value }})</sub>
edit-mode: replace
長いな…
deploy
check-if-pr-exists
comment
の 3 つの job があり、直列になっています。
deploy
では実際のデプロイの処理を GitHub のデプロイ開始、終了の処理で挟み込んでいます。
check-if-pr-exists
では、指定の author、ブランチ名で開いている PR が存在するかをチェックし、存在していればそのような PR の最初のものの番号を、なければ 0 を返します。
needs
に deploy
を指定することで、デプロイ完了時にはじめて PR の存在チェックをするようにします。
comment
は、check-if-pr-exists
で取得した PR 番号と deploy
で取得したデプロイ先の URL にもとづき、PR が存在すれば URL をコメントに残します。
PR を開いたときのワークフローでコメントを残すので、そのコメントの ID を peter-evans/find-comment
で取得し、そのコメントを新しいコメント内容で更新するようにしています。
これで main
以外のブランチに push したときに自動でデプロイされ、完了時に PR が開かれていればコメントされます。
また、GitHub の Environments 欄にこんな感じにデプロイが表示されたりします。
PR を開いたときの処理
PR を開いたときには、デプロイが完了しているか確認し、結果によって異なるコメントを PR に残します。
デプロイ完了時にコメントを残す処理は push のワークフローに定義しているため、ここでは考えなくても大丈夫です。
コード例は以下のようになります。
name: Comment on PR about deployment
on:
pull_request:
types:
- opened
env:
ENV_NAME: preview-${{ github.head_ref }} # preview-[branch name]
jobs:
check-if-deployment-exists:
name: Check if a successful deployment exists
runs-on: ubuntu-latest
permissions:
deployments: read
env:
GH_TOKEN: ${{ github.token }}
outputs:
state: ${{ steps.get-status.outputs.state }}
url: ${{ steps.get-status.outputs.url }}
steps:
- name: Get associated deployments
id: get-deployments
run: >
echo "deployments=$(gh api -H "Accept: application/vnd.github.v3+json"
/repos/${{ github.repository }}/deployments?environment=${{ env.ENV_NAME }})" >> $GITHUB_OUTPUT
- name: Get statuses of the deployment
id: get-statuses
run: >
echo "statuses=$(gh api -H "Accept: application/vnd.github.v3+json"
/repos/${{ github.repository }}/deployments/${{ env.DEP_ID }}/statuses)" >> $GITHUB_OUTPUT
env:
DEP_ID: ${{ fromJSON(steps.get-deployments.outputs.deployments)[0].id }}
- name: Get latest status of the deployment
id: get-status
run: |
echo "state=${{ fromJSON(env.STATUS).state }}" >> $GITHUB_OUTPUT
echo "url=${{ fromJSON(env.STATUS).environment_url }}" >> $GITHUB_OUTPUT
env:
STATUS: ${{ toJSON(fromJSON(steps.get-statuses.outputs.statuses)[0]) }}
comment:
needs: check-if-deployment-exists
name: Comment on PR
if: needs.check-if-deployment-exists.outputs.state != null
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get datetime for now
id: datetime
run: echo "value=$(date)" >> $GITHUB_OUTPUT
env:
TZ: Asia/Tokyo
- name: Comment on PR if the deployment has completed
if: needs.check-if-deployment-exists.outputs.state == 'success'
uses: peter-evans/create-or-update-comment@v2
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
:eyes: Visit the **preview** for this PR (updated for commit ${{ github.sha }}):
[![View the preview deployment at ${{ needs.check-if-deployment-exists.outputs.url }}](https://img.shields.io/badge/-View_Preview-eee)](${{ needs.check-if-deployment-exists.outputs.url }})
<sub>(:clock3: updated at ${{ steps.datetime.outputs.value }})</sub>
- name: Create comment if the deployment has not completed
if: needs.check-if-deployment-exists.outputs.state == 'in_progress'
uses: peter-evans/create-or-update-comment@v2
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
:hourglass_flowing_sand: preview deployment is ongoing...
こちらは check-if-deployment-exists
と comment
の 2 つの job があり、直列になっています。
check-if-deployment-exists
では GitHub API を使って、対応する deployments を取得し、最新のものの statuses を取得し、その最新のものの state とデプロイ先の URL を取得しています。
取得している情報としては「この PR を出しているブランチにひもづく最新の deployment は今どういう状態か(進行中か完了しているか)」です。
これらの情報を次の job comment
に渡し、デプロイが成功 success
であれば URL をコメント、未完了 in_progress
であればデプロイ中だと示すコメントを残します。
これで PR を開いたときにコメントを残してくれるようになります。
以上で、プレビューデプロイを作成し PR にコメントする処理を GitHub Actions で実行することができるようになりました。
他にも、PR がマージされたらプレビューデプロイを削除したり、リリースを作成したら production 環境にデプロイしたりといった Action を作ることができます。
この記事が参考になったらうれしいです。
ではまた 👋
参考
Discussion