Sansan Builders Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

【R&D DevOps通信】GitHub Actions: コメント駆動のワークフローを作る

DSOC R&Dの島です。ArcグループにてR&Dエンジニアを務めています。この記事を皮切りにR&D DevOps通信と題しまして、Arcグループの面々が、当社R&Dのエンジニアリングをよりよくするテーマで連載していく予定です。乞うご期待。

私は過去に旅行記しか書いてない人ですが*1*2、今回は初めて技術らしい記事を書きます。GitHubのpull requestへのコメントで発動するワークフロー を GitHub Actions で作ります。

github.co.jp

下のようなイメージです。コメントに反応してコメントを返すアクションを定義しています。本記事は /deploy staging /deploy production のようなコメントによってデプロイに利用することを念頭に置いています。

f:id:sansan_shima:20210129232708p:plain
コメント駆動ワークフローのイメージ

私は最近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 ) になってしまいます。

参考: Events that trigger workflows - GitHub Docs

GITHUB_REF = Default branch となっています。

これも前述の参考記事に沿って、GitHubのAPI (Pulls - GitHub Docs) を利用して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

github.com

github.com

では、先ほどのフローをこの slash-command-actionpull-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
          })

f:id:sansan_shima:20210130083312p:plain
コメントによる通知の例

サードパーティーアクションの参照について

本記事では、サードパーティーアクションはコミットSHAで指定しています。user/foo-action@v1 のような指定は極力避けるのが望ましいです。

-  uses: xt0rted/pull-request-comment-branch@29fe0354c01b30fb3de76f193ab33abf8fe5ddb0

また、サードパーティーアクションは使用前に必ず実装を確認しましょう。問題ないと判断したら、その状態のコミットSHAで指定します。

actions/checkout のようなGitHub本家や認証済みの開発元(Verified creator)によるアクションについては、信頼できるとみなし @v2 のような指定をしています。

このようなサードパーティーアクションに関わるセキュリティについては、以下のページを参照してください。以上3点についてはいずれも説明されています。

docs.github.com

サードパーティーアクションが多数ある点、言い換えると誰でも簡単に独自のアクションを作って公開できる点は、GitHub Actionsの大きな強みです。注意しつつうまく活用しましょう。

GitHubのステータスに通知する

以上で最低限の運用に乗りそうな感じですが、もう1つ欲を出しましょう。ステータス通知を設定します。ステータスというのは以下のようなものです。Detailsを押すとワークフローの進捗や結果を見に行くことができます。

f:id:sansan_shima:20210201183839p:plain
GitHubステータス表示のイメージ

on: push など定番のトリガーでは当たり前のようにステータスが付いてくれますが、issue_comment は付いてくれません。pull request対象のブランチの取得に一苦労したことからもわかるように、そもそも本来pull requestに紐づいていないためです。

そこで、GitHubのStatuses APIを使用して自前で行います。APIリファレンスを見れば自分でcurl等で叩くことも難しくありませんが、これも先人のActionを使わせてもらいます。

github.com

以下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 のステータスを通知します。

f:id:sansan_shima:20210130083423p:plain
処理中のステータス通知の例

処理が進んで最後まで進むと、success のステータスを通知します。もし失敗すれば error_notification のジョブへ進み、failure のステータスを通知します。

f:id:sansan_shima:20210201183659p:plain
正常終了時のステータス通知の例

問題点

コメント駆動のワークフローには、いくつか問題点があります。

ワークフローが複雑になりがち

ここまでを見てわかるように、使い心地を良くしようとすると複雑なYAMLと化します。すべては、ここまで述べたように元来 issue_comment トリガーと pull request は連携がなく、何でも自分で書く必要があることに起因します。

無関係のコメントもアクション履歴に残る

Actionsメニューから、これまでのワークフローの履歴を一覧できます。

実は下の図では /deploy を書いたことによるのは一番下だけで、他は「こんにちは」のような無関係なコメントに反応したものです。アクションが起動してから/deployという文字列かどうか判定しているので、起動した履歴には残ってしまうのです。 xt0rted/slash-command-action では、指定の文字列を含まないコメントだとエラー ❌ として扱われます。本記事の最初に挙げたワークフローのようにjobのif条件を使った場合はスキップになりますが、いずれにせよ履歴には残るので、履歴はゴミだらけになるのが避けられません。

f:id:sansan_shima:20210130003756p:plain
アクション履歴

解決しようとした軌跡

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 のようなコメントによってデプロイに利用することを念頭に置いています。私の調べでは、デプロイのワークフローとしてよく取られるのは 特定のブランチに意味を持たせる 方法です。以下の記事がよくまとまっていて参考になりました。

techblog.exawizards.com

ここでいえば当グループでは 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だけが特別だと捉えています。

© Sansan, Inc.