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 "::set-output name=pulls::
68
$(gh api -H "Accept: application/vnd.github.v3+json" -X GET
69
/repos/${{ github.repository }}/pulls -f state=open -f head=${{ github.actor }}:${{ github.ref_name }})"
70
env:
71
GH_TOKEN: ${{ github.token }}
72
73
- name: Get PR number
74
id: get-pr-number
75
run: |
76
if [ "${{ fromJSON(env.PULL) }}" ]; then
77
echo "::set-output name=pr-number::${{ fromJSON(env.PULL).number }}";
78
else
79
echo "::set-output name=pr-number::0";
80
fi
81
env:
82
PULL: ${{ toJSON(fromJSON(steps.prs.outputs.pulls)[0]) }}
83
84
comment:
85
needs:
86
- deploy
87
- check-if-pr-exists
88
89
name: Comment on PR
90
91
if: needs.check-if-pr-exists.outputs.pr-number != 0
92
93
runs-on: ubuntu-latest
94
95
permissions:
96
contents: read # actions/checkout needs this
97
issues: write # peter-evans/create-or-update-comment needs this
98
pull-requests: write # peter-evans/create-or-update-comment needs this
99
100
steps:
101
- name: Checkout
102
uses: actions/checkout@v3
103
104
- name: Find Comment
105
uses: peter-evans/find-comment@v2
106
id: fc
107
with:
108
issue-number: ${{ needs.check-if-pr-exists.outputs.pr-number }}
109
comment-author: github-actions[bot]
110
body-includes: preview
111
112
- name: Get datetime for now
113
id: datetime
114
run: echo "::set-output name=value::$(date)"
115
env:
116
TZ: Asia/Tokyo
117
118
- name: Create or update comment
119
uses: peter-evans/create-or-update-comment@v2
120
with:
121
issue-number: ${{ needs.check-if-pr-exists.outputs.pr-number }}
122
comment-id: ${{ steps.fc.outputs.comment-id }}
123
body: |
124
:eyes: Visit the **preview** for this PR (updated for commit ${{ github.sha }}):
125
[![View the preview deployment at ${{ needs.deploy.outputs.url }}](https://img.shields.io/badge/-View_Preview-eee)](${{ needs.deploy.outputs.url }})
126
<sub>(:clock3: updated at ${{ steps.datetime.outputs.value }})</sub>
127
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 "::set-output name=deployments::
32
$(gh api -H "Accept: application/vnd.github.v3+json"
33
/repos/${{ github.repository }}/deployments?environment=${{ env.ENV_NAME }})"
34
35
- name: Get statuses of the deployment
36
id: get-statuses
37
run: >
38
echo "::set-output name=statuses::
39
$(gh api -H "Accept: application/vnd.github.v3+json"
40
/repos/${{ github.repository }}/deployments/${{ env.DEP_ID }}/statuses)"
41
env:
42
DEP_ID: ${{ fromJSON(steps.get-deployments.outputs.deployments)[0].id }}
43
44
- name: Get latest status of the deployment
45
id: get-status
46
run: |
47
echo "::set-output name=state::${{ fromJSON(env.STATUS).state }}"
48
echo "::set-output name=url::${{ fromJSON(env.STATUS).environment_url }}"
49
env:
50
STATUS: ${{ toJSON(fromJSON(steps.get-statuses.outputs.statuses)[0]) }}
51
52
comment:
53
needs: check-if-deployment-exists
54
55
name: Comment on PR
56
57
if: needs.check-if-deployment-exists.outputs.state != null
58
59
runs-on: ubuntu-latest
60
61
permissions:
62
contents: read
63
issues: write
64
pull-requests: write
65
66
steps:
67
- name: Checkout
68
uses: actions/checkout@v3
69
70
- name: Get datetime for now
71
id: datetime
72
run: echo "::set-output name=value::$(date)"
73
env:
74
TZ: Asia/Tokyo
75
76
- name: Comment on PR if the deployment has completed
77
if: needs.check-if-deployment-exists.outputs.state == 'success'
78
uses: peter-evans/create-or-update-comment@v2
79
with:
80
issue-number: ${{ github.event.pull_request.number }}
81
body: |
82
:eyes: Visit the **preview** for this PR (updated for commit ${{ github.sha }}):
83
[![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 }})
84
<sub>(:clock3: updated at ${{ steps.datetime.outputs.value }})</sub>
85
86
- name: Create comment if the deployment has not completed
87
if: needs.check-if-deployment-exists.outputs.state == 'in_progress'
88
uses: peter-evans/create-or-update-comment@v2
89
with:
90
issue-number: ${{ github.event.pull_request.number }}
91
body: |
92
: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毎にプレビューデプロイを行う