DSOC R&Dの島です。ArcグループにてR&Dエンジニアを務めています。この記事を皮切りにR&D DevOps通信と題しまして、Arcグループの面々が、当社R&Dのエンジニアリングをよりよくするテーマで連載していく予定です。乞うご期待。
私は過去に旅行記しか書いてない人ですが*1*2、今回は初めて技術らしい記事を書きます。GitHubのpull requestへのコメントで発動するワークフロー を GitHub Actions で作ります。
下のようなイメージです。コメントに反応してコメントを返すアクションを定義しています。本記事は /deploy staging
/deploy production
のようなコメントによってデプロイに利用することを念頭に置いています。
私は最近GitHub Actionsに入れ込んでおり、個人開発で使っていた CircleCI, Travis CI, AppVeyor は全部GitHub Actionsに移行しました。どれもお世話になりましたし今でも良いサービスですが、今後は全てGitHub Actionsでいいのではないかというのが2021年現在の所感で、当グループでも活用を深めていきます。
先に結論を申せば、結局このコメント駆動方式は当グループでは見送りました。 理由は後述しますが、うまくはまるケースもあると思いますので書き残しておきます。
ワークフローの自前実装
GitHub Actionsの概要は割愛し、早速ワークフローの開発に入ります。
今回は、pull requestに /deploy
とコメントされたらアクションが発動するようにします。
pull requestへのコメントで発火させる
方法についてはこちらの記事によくまとまっています。 akaimo.hatenablog.jp
正直なところ口を出す余地がありませんが、なぞって作成していきましょう。.github/workflows/<好きな名前>.yml
という名前でファイルを作り、編集していきます。
手元のテキストエディタで行うよりも、多少支援が得られるのでGitHubのWebページにて直接編集するのがおすすめです。Actions -> New workflow
からテンプレートを選ぶ画面に進めます。set up a workflow yourself
を押して自分で編集を始めます。
以下のように記述します。発火するアクションとしてとりあえずechoしています。
name: "Deploy" on: issue_comment: types: [created, edited] jobs: deploy: if: github.event_name == 'issue_comment' && contains(github.event.comment.html_url, '/pull/') && startsWith(github.event.comment.body, '/deploy') ) runs-on: ubuntu-latest steps: - run: | echo "デプロイを開始しました (${{ github.event.comment.body }})"
ここでのポイントは以下です。
on: issue_comment
により、issueまたはpull requestのコメントが操作されたことをトリガーにできます。- jobの
if
を指定することで、pull requestの場合のみ、かつ、指定したコメント文字列の場合のみ、という実行条件を付けます。
pull requestのブランチを対象にする
issue_comment
をトリガーとしてアクションが起動した場合、ブランチはデフォルトブランチ (一般的には main
または master
) になってしまいます。
参考: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#issue_comment
GITHUB_REF
= Default branch となっています。
これも前述の参考記事に沿って、GitHubのAPI (https://docs.github.com/en/rest/reference/pulls#get-a-pull-request) を利用してpull request対象のブランチ情報を取得します。何かと使うので、ブランチ名に加えて最新コミットのshaも取得しておきました。
name: "Deploy" on: issue_comment: types: [created, edited] jobs: deploy: if: github.event_name == 'issue_comment' && contains(github.event.comment.html_url, '/pull/') && startsWith(github.event.comment.body, '/deploy') ) runs-on: ubuntu-latest steps: - name: "Get branch name and sha" id: get_branch run: | PR=$(curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" ${{ github.event.issue.pull_request.url }}) echo "::set-output name=branch::$(echo $PR | jq -r '.head.ref')" echo "::set-output name=sha::$(echo $PR | jq -r '.head.sha')" - name: "Checkout" uses: actions/checkout@v2 with: ref: ${{ steps.get_branch.outputs.branch }} - run: | echo "デプロイを開始しました (${{ github.event.comment.body }})"
既存のActionを使う
ここまでで、pull requestへのコメントにより発火する原理は押さえました。多少込み入ったワークフローになることがわかりますが、なんとこのコメントトリガーをサポートしたり、ブランチ名を取得するアクションが既にあります。 *3
では、先ほどのフローをこの slash-command-action
と pull-request-comment-branch
を使って書き換えてみます。ついでに、actions/github-scriptを使用して、pull requestへのコメントで通知してみましょう。READMEにコメントを投稿する例があるので苦労しません。
name: "Deploy" on: issue_comment: types: [created, edited] jobs: deploy: runs-on: ubuntu-latest steps: - name: "Check for Command" id: command uses: xt0rted/slash-command-action@065da288bcfe24ff96b3364c7aac1f6dca0fb027 #1.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} command: deploy reaction: "true" reaction-type: "eyes" - name: "Get branch name" uses: xt0rted/pull-request-comment-branch@29fe0354c01b30fb3de76f193ab33abf8fe5ddb0 #1.2.0 id: comment-branch with: repo_token: ${{ secrets.GITHUB_TOKEN }} - name: "Checkout" uses: actions/checkout@v2 with: ref: ${{ steps.get_branch.outputs.head_ref }} - name: "Post Comment" uses: actions/github-script@v3 env: MESSAGE: | デプロイを開始しました (名前:${{ steps.command.outputs.command-name }}, 引数:${{ steps.command.outputs.command-arguments }}) ブランチ: ${{ steps.comment-branch.outputs.head_ref }} SHA: ${{ steps.comment-branch.outputs.head_sha }} with: github-token: ${{secrets.GITHUB_TOKEN}} script: | github.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: process.env.MESSAGE })
サードパーティーアクションの参照について
本記事では、サードパーティーアクションはコミットSHAで指定しています。user/foo-action@v1
のような指定は極力避けるのが望ましいです。
- uses: xt0rted/pull-request-comment-branch@29fe0354c01b30fb3de76f193ab33abf8fe5ddb0
また、サードパーティーアクションは使用前に必ず実装を確認しましょう。問題ないと判断したら、その状態のコミットSHAで指定します。
actions/checkout
のようなGitHub本家や認証済みの開発元(Verified creator)によるアクションについては、信頼できるとみなし @v2
のような指定をしています。
このようなサードパーティーアクションに関わるセキュリティについては、以下のページを参照してください。以上3点についてはいずれも説明されています。
https://docs.github.com/ja/actions/learn-github-actions/security-hardening-for-github-actionsdocs.github.com
サードパーティーアクションが多数ある点、言い換えると誰でも簡単に独自のアクションを作って公開できる点は、GitHub Actionsの大きな強みです。注意しつつうまく活用しましょう。
GitHubのステータスに通知する
以上で最低限の運用に乗りそうな感じですが、もう1つ欲を出しましょう。ステータス通知を設定します。ステータスというのは以下のようなものです。Details
を押すとワークフローの進捗や結果を見に行くことができます。
on: push
など定番のトリガーでは当たり前のようにステータスが付いてくれますが、issue_comment
は付いてくれません。pull request対象のブランチの取得に一苦労したことからもわかるように、そもそも本来pull requestに紐づいていないためです。
そこで、GitHubのStatuses APIを使用して自前で行います。APIリファレンスを見れば自分でcurl等で叩くことも難しくありませんが、これも先人のActionを使わせてもらいます。
以下3つのタイミングでステータスを更新します。
- 処理の開始前(コミットのSHA取得後)に、処理中 (pending) のステータス通知
- 処理の正常終了時に、処理完了 (success) のステータス通知
- 処理の異常終了時に、処理失敗 (failure) のステータス通知
name: "Deploy" on: issue_comment: types: [created, edited] jobs: # 対象のブランチを決定 prerequisites: name: "Prerequisites" runs-on: ubuntu-latest steps: - name: "Check for Command" id: command uses: xt0rted/slash-command-action@065da288bcfe24ff96b3364c7aac1f6dca0fb027 #1.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} command: deploy reaction: "true" reaction-type: "eyes" - name: "Get upstream branch" uses: xt0rted/pull-request-comment-branch@29fe0354c01b30fb3de76f193ab33abf8fe5ddb0 #1.2.0 id: upstream_branch with: repo_token: ${{ secrets.GITHUB_TOKEN }} - name: "Notify pending status" uses: hkusu/status-create-action@1040f888115fc10b2e0fa8efc8ef3b85a916af2e #1.0.0 with: sha: ${{ steps.upstream_branch.outputs.sha }} state: pending description: Branch:${{steps.upstream_branch.outputs.branch_name}} context: Deployment outputs: command_name: ${{ steps.command.outputs.command-name }} command_arguments : ${{ steps.command.outputs.command-arguments }} branch_name: ${{ steps.upstream_branch.outputs.head_ref }} commit_sha: ${{ steps.upstream_branch.outputs.head_sha }} # デプロイ処理 deploy: needs: [prerequisites] runs-on: ubuntu-latest steps: - name: "Checkout" uses: actions/checkout@v2 with: ref: ${{ needs.prerequisites.outputs.branch_name }} - name: "Post Comment (Start)" uses: actions/github-script@v3 env: MESSAGE: | デプロイを開始しました (名前:${{ needs.prerequisites.outputs.command_name }}, 引数:${{ needs.prerequisites.outputs.command_arguments }}) ブランチ: ${{ needs.prerequisites.outputs.branch_name }} SHA: ${{ needs.prerequisites.outputs.commit_sha }} with: github-token: ${{secrets.GITHUB_TOKEN}} script: | github.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: process.env.MESSAGE }) - name: 'Deploy' run: | sleep 30 # デプロイ作業のつもり - name: "Notify successful status" uses: hkusu/status-create-action@1040f888115fc10b2e0fa8efc8ef3b85a916af2e with: sha: ${{ needs.prerequisites.outputs.commit_sha }} state: success context: Deployment description: Successful - name: "Post Comment (End)" uses: actions/github-script@v3 env: MESSAGE: | デプロイが完了しました with: github-token: ${{secrets.GITHUB_TOKEN}} script: | github.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: process.env.MESSAGE }) # エラーの際の通知 error_notification: name: "Notify failure status" if: failure() && needs.prerequisites.outputs.commit_sha != null needs: [prerequisites, deploy] runs-on: ubuntu-latest steps: - name: Notify pending status uses: hkusu/status-create-action@1040f888115fc10b2e0fa8efc8ef3b85a916af2e with: sha: ${{ needs.prerequisites.outputs.commit_sha }} state: failure description: "Failed" context: Deployment
一気に長くなりました。try-catch的な記述のセオリーがわかっていませんが、そういう挙動を目指しています。jobを分けて、needs
で依存関係を設定したり if: failure()
で失敗時のみの処理を書いたりするのがポイントです。
ブランチ情報を取得でき次第、pending のステータスを通知します。
処理が進んで最後まで進むと、success のステータスを通知します。もし失敗すれば error_notification
のジョブへ進み、failure のステータスを通知します。
問題点
コメント駆動のワークフローには、いくつか問題点があります。
ワークフローが複雑になりがち
ここまでを見てわかるように、使い心地を良くしようとすると複雑なYAMLと化します。すべては、ここまで述べたように元来 issue_comment
トリガーと pull request は連携がなく、何でも自分で書く必要があることに起因します。
無関係のコメントもアクション履歴に残る
Actionsメニューから、これまでのワークフローの履歴を一覧できます。
実は下の図では /deploy
を書いたことによるのは一番下だけで、他は「こんにちは」のような無関係なコメントに反応したものです。アクションが起動してから/deploy
という文字列かどうか判定しているので、起動した履歴には残ってしまうのです。 xt0rted/slash-command-action では、指定の文字列を含まないコメントだとエラー ❌ として扱われます。本記事の最初に挙げたワークフローのようにjobのif条件を使った場合はスキップになりますが、いずれにせよ履歴には残るので、履歴はゴミだらけになるのが避けられません。
解決しようとした軌跡
もしワークフローの名前 (YAMLの一番上で定義する) を動的に変えられると、一覧にしたとき多少見通しが良くなるかと思いましたが、試したところできませんでした。 github.community
この
issue_comment
によるワークフローを監視する別のワークフローを作って、無関係なコメントによる発動なら削除するようにすれば解決かもしれません(未確認)。
name: "Issue Comment Watcher" on: workflow_run: workflows: ["Deploy"] types: - completed jobs: watch: runs-on: ubuntu-latest steps: - run: | # コメント駆動ワークフローの状態を見て、無関係なら消す処理
第三者がコメントしても発動しないように要考慮
企業内等で利用しているprivateリポジトリであればほぼ問題にならないでしょう。publicリポジトリの場合は、任意の第三者がコメントしてくることが考えられるので、特定の権限を持ったユーザのコメントのみを受け付ける必要があります。
xt0rted/slash-command-action では、permission-level
というパラメータによって必要とする権限レベルを設定することができます。
- name: Check for Command id: command uses: xt0rted/slash-command-action@065da288bcfe24ff96b3364c7aac1f6dca0fb027 with: repo-token: ${{ secrets.GITHUB_TOKEN }} command: deploy reaction: "true" reaction-type: "eyes" permission-level: admin
main(master)ブランチでYAMLを書き換えないと効かない
on: push
などのトリガーは、どのブランチでワークフローYAMLを書き換えてpushしても即座に反映されます。しかし on: issue_comment
については、デフォルトブランチにあるYAMLを更新しなければなりません。
参考:https://docs.github.com/en/actions/reference/events-that-trigger-workflows#issue_comment
Note: This event will only trigger a workflow run if the workflow file is on the default branch.
動作チェックにあたっては、mainブランチに直pushしまくるようなことになります。
利点
利用者にとってはお手軽 - 開発体制、規模、設計などによるところが大きいですが、当グループにおいては馴染みやすい方法だと思われました。
本記事は /deploy staging
のようなコメントによってデプロイに利用することを念頭に置いています。私の調べでは、デプロイのワークフローとしてよく取られるのは 特定のブランチに意味を持たせる 方法です。以下の記事がよくまとまっていて参考になりました。
ここでいえば当グループでは GitHub Flow が馴染むと考え、そのサポートとしてコメント駆動デプロイは有望な選択肢と捉えました。
- 部署がR&Dであり研究員メンバーが多く、gitの扱いに熟達した者が限られます。「pull requestをとにかくmainに向けて出す」「適当なタイミングで
/deploy
と打ち込む」というシンプルなルールが良いと考えました。 - GitHubにはGitHub Flowが最善だろうとの考えが私にありました。検索がmainブランチでしか行えないといったGitHubの仕様を鑑みるに、mainブランチ以外の特別なブランチは設けないのがスムーズに回せると思いました。*4
- DSOC R&D部門ではサービスごとにリポジトリは分散しており、それぞれに関わるメンバーは数人程度と限られます。同一リポジトリでpull requestが日々乱立する状況にはなく、素朴な方法のほうが運用しやすいと考えました。
おわりに
on: issue_comment
をトリガーとするワークフローにより、GitHubのpull requestへのコメント投稿で起動する処理を作成できます。- pull requestのブランチ取得、通知の方法まで面倒を見る必要があり、ワークフローは複雑になります。ただしサードパーティーアクションの活用により幾分簡単になります。
- 困ったらGitHubの生のAPIを叩けば、だいたいねじ伏せられます。もちろんそれは他のCIでも可能ですが、豊富なイベントトリガーや付随するコンテクスト情報、
secrets.GITHUB_TOKEN
をサッと取り出してcurlできてしまう、等々が強みです。 - 前述の利点・欠点を天秤にかけ、当グループではこのコメント駆動アクションの採用を見送りました。特にアクション履歴が汚れる点を考慮しましたが、そこを気にしないのなら良いかもしれません。
では結局どうしたのか。また機会がありましたら、別のイベントトリガーによるワークフローや、実際の活用例等についても述べたいと思います。
*1:https://buildersbox.corp-sansan.com/entry/2019/06/04/110000
*2:https://buildersbox.corp-sansan.com/entry/2018/12/27/113000
*3:ちなみにこの作者の xt0rted さんは他にも有用なアクションを多数公開されていて、ぜひ一見の価値ありです。https://github.com/xt0rted/actions
*4:正確にはmainとgh-pagesだけが特別だと捉えています。