GitHub Actions で Vercel bot みたいなプレビューデプロイをつくる

Vercel というウェブサイトをデプロイできるサービスがあって、それを GitHub と連携すると、

GitHub で push したりプルリクエストを開いたりしたときに、自動でいろいろやってくれるんですよ。

デフォルトブランチに push したら自動で再デプロイしてくれたり

deploy

デフォルト以外のブランチに push したら自動で preview version をデプロイしてくれて、

PR を開くと自動でコメントしてくれたり

PR comment

再度 push すると再度プレビューをデプロイしてくれたりします。

特に PR 開くと自動でプレビューをつくってくれるのは、さくっと動作確認できてレビューにもすごく便利です。

Vercel を使っているときはこれを GitHub のリポジトリと連携するだけで、特に設定しなくてもこういう便利なことをやってくれるんですが、

使っていないときにもこれができたらなあと思ったわけです。

ということで、GitHub Actions でこのプレビューデプロイと似たようなことをやってみましょう!

そもそも vercel[bot] は何をやっているのか

どの GitHub のイベントがどういうふうにプレビューデプロイや PR コメントをトリガーしているのか観察してみると、

  • push したときにデプロイがはじまる
  • PR を開いたときにコメントする
    • デプロイが終わっていなければ Preview: In Progress とコメントする
    • デプロイが終わり次第コメントを Preview URL をのせたものに更新する
    • デプロイがすでに終わっていれば Preview URL をすぐにコメントする

こんな感じかと思います。

PR を開いたときにはじめてデプロイをするという方法もあり、ワークフローがひとつで済むので簡単ですが、

push してから PR を開くまでの間にデプロイをすすめておいて、PR を開いたらすぐにコメントしてくれるほうが時短になるので、これを実現したいです。

処理の内容を図にすると次のようになります。

workflows-diagram

デプロイが終わるのと PR を開くのと、どちらが先に終わるかによって通るフローが変わってきます。

デプロイが先に終わっていれば PR が開かれたらすぐに preview URL をコメントするし、

PR が先に開いていればデプロイが完了するまではデプロイ中であるとコメントし、デプロイが完了すれば preview URL をのせたコメントに更新します。

ワークフローを書く

見たところワークフローが GitHub に対して必要になる処理は、

  • PR にコメントを作成・更新する
  • デプロイが完了しているか確認する
  • 対応する PR が開いているか確認する

いずれも GitHub API を使うことで実現できます。

PR コメントに関しては peter-evans/find-commentpeter-evans/create-or-update-comment を使うのが簡単かと思います。

「デプロイが完了しているか確認する」に関しては、

GitHub の environment を作成して、デプロイ前に「デプロイ中」のステータスを付与し、デプロイ後に「完了」のステータスを付与することで、

GitHub API によってデプロイ状態を取得できます。

これは bobheadxi/deployments を使うと簡単です。

今回のコード例ではこれらを使用します。

push したときの処理

push したときには、デプロイをし、デプロイ後にそのブランチに対応する PR が存在するかチェックし、存在していたらコメントを残します。

コード例は以下のようになります。

.github/workflows/deploy-preview.yml
1
name: Deploy preview
2
3
on:
4
push:
5
branches: # branches other than main
6
- '*'
7
- '!main'
8
9
env:
10
ENV_NAME: preview-${{ github.ref_name }} # preview-[branch name]
11
12
jobs:
13
deploy:
14
name: Deploy
15
16
runs-on: ubuntu-latest
17
18
permissions:
19
deployments: write # bobheadxi/deployments needs this
20
21
outputs:
22
url: ${{ steps.deploy-url.outputs.url }}
23
24
steps:
25
- name: Start deployment
26
uses: bobheadxi/deployments@v1
27
id: deployment
28
with:
29
step: start
30
token: ${{ github.token }}
31
env: ${{ env.ENV_NAME }}
32
33
# Authenticate and deploy steps...
34
35
- name: Deploy
36
id: deploy-url
37
run: # Output the deployment URL
38
39
- name: Update deployment status
40
uses: bobheadxi/deployments@v1
41
if: always()
42
with:
43
step: finish
44
token: ${{ github.token }}
45
status: ${{ job.status }}
46
env: ${{ steps.deployment.outputs.env }}
47
env_url: ${{ steps.deploy-url.outputs.url }}
48
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
49
50
check-if-pr-exists:
51
needs: deploy
52
53
name: Check if associated PR exists
54
55
runs-on: ubuntu-latest
56
57
permissions:
58
pull-requests: read
59
60
outputs:
61
pr-number: ${{ steps.get-pr-number.outputs.pr-number }}
62
63
steps:
64
- name: Get associated PRs
65
id: prs
66
run: >
67
echo "pulls=$(gh api -H "Accept: application/vnd.github.v3+json" -X GET
68
/repos/${{ github.repository }}/pulls
69
-f state=open
70
-f head=${{ github.repository_owner }}:${{ github.ref_name }})" >> $GITHUB_OUTPUT
71
env:
72
GH_TOKEN: ${{ github.token }}
73
74
- name: Get PR number
75
id: get-pr-number
76
run: |
77
if [ "${{ fromJSON(env.PULL) }}" ]; then
78
echo "pr-number=${{ fromJSON(env.PULL).number }}" >> $GITHUB_OUTPUT;
79
else
80
echo "pr-number=0" >> $GITHUB_OUTPUT;
81
fi
82
env:
83
PULL: ${{ toJSON(fromJSON(steps.prs.outputs.pulls)[0]) }}
84
85
comment:
86
needs:
87
- deploy
88
- check-if-pr-exists
89
90
name: Comment on PR
91
92
if: needs.check-if-pr-exists.outputs.pr-number != 0
93
94
runs-on: ubuntu-latest
95
96
permissions:
97
contents: read # actions/checkout needs this
98
issues: write # peter-evans/create-or-update-comment needs this
99
pull-requests: write # peter-evans/create-or-update-comment needs this
100
101
steps:
102
- name: Checkout
103
uses: actions/checkout@v3
104
105
- name: Find Comment
106
uses: peter-evans/find-comment@v2
107
id: fc
108
with:
109
issue-number: ${{ needs.check-if-pr-exists.outputs.pr-number }}
110
comment-author: github-actions[bot]
111
body-includes: preview
112
113
- name: Get datetime for now
114
id: datetime
115
run: echo "value=$(date)" >> $GITHUB_OUTPUT
116
env:
117
TZ: Asia/Tokyo
118
119
- name: Create or update comment
120
uses: peter-evans/create-or-update-comment@v2
121
with:
122
issue-number: ${{ needs.check-if-pr-exists.outputs.pr-number }}
123
comment-id: ${{ steps.fc.outputs.comment-id }}
124
body: |
125
:eyes: Visit the **preview** for this PR (updated for commit ${{ github.sha }}):
126
[![View the preview deployment at ${{ needs.deploy.outputs.url }}](https://img.shields.io/badge/-View_Preview-eee)](${{ needs.deploy.outputs.url }})
127
<sub>(:clock3: updated at ${{ steps.datetime.outputs.value }})</sub>
128
edit-mode: replace

長いな…

deploy check-if-pr-exists comment の 3 つの job があり、直列になっています。

deploy では実際のデプロイの処理を GitHub のデプロイ開始、終了の処理で挟み込んでいます。

check-if-pr-exists では、指定の author、ブランチ名で開いている PR が存在するかをチェックし、存在していればそのような PR の最初のものの番号を、なければ 0 を返します。

needsdeploy を指定することで、デプロイ完了時にはじめて PR の存在チェックをするようにします。

comment は、check-if-pr-exists で取得した PR 番号と deploy で取得したデプロイ先の URL にもとづき、PR が存在すれば URL をコメントに残します。

PR を開いたときのワークフローでコメントを残すので、そのコメントの ID を peter-evans/find-comment で取得し、そのコメントを新しいコメント内容で更新するようにしています。

これで main 以外のブランチに push したときに自動でデプロイされ、完了時に PR が開かれていればコメントされます。

また、GitHub の Environments 欄にこんな感じにデプロイが表示されたりします。

GitHub Environments

PR を開いたときの処理

PR を開いたときには、デプロイが完了しているか確認し、結果によって異なるコメントを PR に残します。

デプロイ完了時にコメントを残す処理は push のワークフローに定義しているため、ここでは考えなくても大丈夫です。

コード例は以下のようになります。

.github/workflows/comment-on-pr.yml
1
name: Comment on PR about deployment
2
3
on:
4
pull_request:
5
types:
6
- opened
7
8
env:
9
ENV_NAME: preview-${{ github.head_ref }} # preview-[branch name]
10
11
jobs:
12
check-if-deployment-exists:
13
name: Check if a successful deployment exists
14
15
runs-on: ubuntu-latest
16
17
permissions:
18
deployments: read
19
20
env:
21
GH_TOKEN: ${{ github.token }}
22
23
outputs:
24
state: ${{ steps.get-status.outputs.state }}
25
url: ${{ steps.get-status.outputs.url }}
26
27
steps:
28
- name: Get associated deployments
29
id: get-deployments
30
run: >
31
echo "deployments=$(gh api -H "Accept: application/vnd.github.v3+json"
32
/repos/${{ github.repository }}/deployments?environment=${{ env.ENV_NAME }})" >> $GITHUB_OUTPUT
33
34
- name: Get statuses of the deployment
35
id: get-statuses
36
run: >
37
echo "statuses=$(gh api -H "Accept: application/vnd.github.v3+json"
38
/repos/${{ github.repository }}/deployments/${{ env.DEP_ID }}/statuses)" >> $GITHUB_OUTPUT
39
env:
40
DEP_ID: ${{ fromJSON(steps.get-deployments.outputs.deployments)[0].id }}
41
42
- name: Get latest status of the deployment
43
id: get-status
44
run: |
45
echo "state=${{ fromJSON(env.STATUS).state }}" >> $GITHUB_OUTPUT
46
echo "url=${{ fromJSON(env.STATUS).environment_url }}" >> $GITHUB_OUTPUT
47
env:
48
STATUS: ${{ toJSON(fromJSON(steps.get-statuses.outputs.statuses)[0]) }}
49
50
comment:
51
needs: check-if-deployment-exists
52
53
name: Comment on PR
54
55
if: needs.check-if-deployment-exists.outputs.state != null
56
57
runs-on: ubuntu-latest
58
59
permissions:
60
contents: read
61
issues: write
62
pull-requests: write
63
64
steps:
65
- name: Checkout
66
uses: actions/checkout@v3
67
68
- name: Get datetime for now
69
id: datetime
70
run: echo "value=$(date)" >> $GITHUB_OUTPUT
71
env:
72
TZ: Asia/Tokyo
73
74
- name: Comment on PR if the deployment has completed
75
if: needs.check-if-deployment-exists.outputs.state == 'success'
76
uses: peter-evans/create-or-update-comment@v2
77
with:
78
issue-number: ${{ github.event.pull_request.number }}
79
body: |
80
:eyes: Visit the **preview** for this PR (updated for commit ${{ github.sha }}):
81
[![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 }})
82
<sub>(:clock3: updated at ${{ steps.datetime.outputs.value }})</sub>
83
84
- name: Create comment if the deployment has not completed
85
if: needs.check-if-deployment-exists.outputs.state == 'in_progress'
86
uses: peter-evans/create-or-update-comment@v2
87
with:
88
issue-number: ${{ github.event.pull_request.number }}
89
body: |
90
:hourglass_flowing_sand: preview deployment is ongoing...

こちらは check-if-deployment-existscomment の 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 を作ることができます。

この記事が参考になったらうれしいです。

ではまた 👋

参考

Cloud Runを使ってPR毎にプレビューデプロイを行う