💫

GitHub Actionsでカスタムコマンドを作ってChatOpsを始めよう(フォーマットをする例)

2024/12/06に公開

アドベントカレンダーです。

https://qiita.com/advent-calendar/2024/optimind

GitHubでChatOpsをぜひやりましょう。ということで、その第一歩として、GitHub Actionsを利用して手軽に実行できるチャットコマンドを作ってみたいと思います。

シチュエーション

GitHubでレビューするときに、ちょっとした編集はGitHubの機能でできます。なので、レビュアー側で片付けてしまいたい。だけれどいざコミットすると行が長くなって改行が必要だった…
といったことがあります。

フォーマットで落ちてしまってスムーズに進められない。
GitHub Codespaceを立ち上げて依存をインストールして…としても良いのですがそれも手間がかかります。

今回はより簡単に、PR内のコメントで「!format」とコメントするだけで解決する方法をGitHub Actionsで実現したいと思います。

!formatコマンド利用デモ

完全版のコード

折り畳みの中に完全版のコードを用意しています。
Node.jsを想定した、prettierを実行するためのコードです。バージョンや npm run の仕方はプロジェクトごとに異なるでしょうから、そのあたりは修正する必要があります。

.github/workflows/command-format.yml
name: 'コマンド: フォーマット'
on:
  issue_comment:
    types: [created]
env:
  NODE_OPTIONS: "--max-old-space-size=4096"
  NODE_VERSION: 20.11.1
jobs:
  command_format:
    if: |
      github.event.comment.user.type != 'Bot'
      && github.event.issue.pull_request != null
      && (
        false
        || (
          github.event.comment.body == '!format'
          || startsWith(github.event.comment.body, '!format ')
          || startsWith(github.event.comment.body, '!format\n')
        )
        || (
          github.event.comment.body == '!prettier'
          || startsWith(github.event.comment.body, '!prettier ')
          || startsWith(github.event.comment.body, '!prettier\n')
        )
      )
    runs-on: ubuntu-latest
    steps:
      - name: PRの情報を取得
        id: pr_info
        run: |
          head_ref=$(gh pr --repo ${{ github.event.repository.full_name }} view ${{ github.event.issue.number }} --json 'headRefName' -q '.headRefName')
          echo "head_ref=$head_ref" >> "$GITHUB_OUTPUT"
        env:
          GH_TOKEN: ${{ github.token }}
      - name: チェックアウト (PR)
        uses: actions/checkout@v4
        with:
          ref: ${{ steps.pr_info.outputs.head_ref }}
          ssh-key: ${{ secrets.COMMAND_ACTIONS_DEPLOY_KEY_SECRET }}

      - name: Use Node.js ${{ env.NODE_VERSION }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - name: "メイン: フォーマットしてプッシュ"
        id: main
        run: |
          corepack npm ci
          corepack npm run _lint:prettier -- --write
          diff_status=$(git diff --exit-code > /dev/null 2>&1; echo $?)
          echo "diff_status=$diff_status" >> "$GITHUB_OUTPUT"
          if [ $diff_status -ne 0 ]; then
            git config user.name "GitHub Action"
            git config user.email "action@github.com"
            git add .
            git commit --message "chore: フォーマット [AUTO]"
            git push
          fi

      - name: 失敗した場合のコメントを追加
        if: failure() && steps.main.outcome == 'failure'
        uses: peter-evans/create-or-update-comment@v4
        with:
          issue-number: ${{ github.event.issue.number }}
          body: |
            @${{ github.event.comment.user.login }}
            ❌フォーマットに失敗しました。
            詳細は[Actionsのログ](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})のジョブ`${{ github.job }}`を確認してください。
            通知タグ: CMD_NOTIFY_ANY, CMD_NOTIFY_FORMAT, CMD_NOTIFY_FORMAT_FAILURE
      - name: 成功した場合のコメントを追加
        if: success() && steps.main.outputs.diff_status != '0'
        uses: peter-evans/create-or-update-comment@v4
        with:
          issue-number: ${{ github.event.issue.number }}
          body: |
            @${{ github.event.comment.user.login }}
            ✅フォーマットに成功しました。
            詳細は[Actionsのログ](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})のジョブ`${{ github.job }}`を確認してください。
            通知タグ: CMD_NOTIFY_ANY, CMD_NOTIFY_FORMAT, CMD_NOTIFY_FORMAT_SUCCESS
      - name: フォーマットの差分がなかった場合のコメントを追加
        if: success() && steps.main.outputs.diff_status == '0'
        uses: peter-evans/create-or-update-comment@v4
        with:
          issue-number: ${{ github.event.issue.number }}
          body: |
            @${{ github.event.comment.user.login }}
            ⏹フォーマットの差分がありませんでした。
            詳細は[Actionsのログ](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})のジョブ`${{ github.job }}`を確認してください。
            通知タグ: CMD_NOTIFY_ANY, CMD_NOTIFY_FORMAT, CMD_NOTIFY_FORMAT_SKIP

解説

イベントの設定

イベントは issue_comment.created[1]を利用します。
また、実行のための if 条件に以下を記載します。

  • github.event.comment.user.type != 'Bot': コメントを書いたユーザーを人間に限定します。無限ループなどを防ぎます。
    • 実行可能な権限をロールで指定したい場合は、
  • github.event.issue.pull_request != null: issue_comment イベントはIssueとPRの両方で発火するので、PR内のものに限定します。
  • github.event.comment.body == '!format': !format という内容のコメントのみに対して発火します。
    • startsWithcontains 関数等を利用して、マッチの仕方を少し緩くすることも考えられます。
on:
  issue_comment:
    types: [created]
jobs:
  command-format:
    if: |
      github.event.comment.user.type != 'Bot'
      && github.event.issue.pull_request != null
      && (
        false
        || (
          github.event.comment.body == '!format'
          || startsWith(github.event.comment.body, '!format ')
          || startsWith(github.event.comment.body, '!format\n')
        )
        || (
          github.event.comment.body == '!prettier'
          || startsWith(github.event.comment.body, '!prettier ')
          || startsWith(github.event.comment.body, '!prettier\n')
        )
      )

PRの情報を取得

- name: PRの情報を取得
  id: pr_info
  run: |
    head_ref=$(gh pr --repo ${{ github.event.repository.full_name }} view ${{ github.event.issue.number }} --json 'headRefName' -q '.headRefName')
    echo "head_ref=$head_ref" >> "$GITHUB_OUTPUT"
  env:
    GH_TOKEN: ${{ github.token }}

今回利用しているのはPR関連の直接のイベントではないため、PRの情報は gh コマンド等から取得する必要があります。

今回はチェックアウトするために、PRが作られたブランチ名 (head_ref) のみ取れれば十分です。他には例えば、マージ先予定のブランチ名 (base_ref) が必要であれば以下のように取得できます。

base_ref=$(gh pr --repo ${{ github.event.repository.full_name }} view ${{ github.event.issue.number }} --json 'baseRefName' -q '.baseRefName')
echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT"

チェックアウト

コメントのイベントなので、チェックアウトは引数に明示的にrefとssh-keyを渡す必要があります。

- name: チェックアウト (PR)
  uses: actions/checkout@v4
  with:
    ref: ${{ steps.pr_info.outputs.head_ref }}
    ssh-key: ${{ secrets.COMMAND_ACTIONS_DEPLOY_KEY_SECRET }}

ref は上記のステップで作った値を用いればよいです。 ssh-key は Deploy Key の秘密鍵です。 https://docs.github.com/en/authentication/connecting-to-github-with-ssh/managing-deploy-keys#set-up-deploy-keys に従い Deploy Key を設定したら、Actions Variablesから COMMAND_ACTIONS_DEPLOY_KEY_SECRET にその秘密鍵を入れてください。

(注意。Deploy Key として入れるのは公開鍵ですが、 ssh-key に入れるのは秘密鍵です)

メインの処理

自動化したいことをやります。ここはもう、それぞれですね。あくまでも私のプロジェクトでの例が以下になります。

- name: Use Node.js ${{ env.NODE_VERSION }}
  uses: actions/setup-node@v3
  with:
    node-version: ${{ env.NODE_VERSION }}
    cache: 'npm'
- name: "メイン: フォーマットしてプッシュ"
  id: main
  run: |
    corepack npm ci
    corepack npm run _lint:prettier -- --write
    diff_status=$(git diff --exit-code > /dev/null 2>&1; echo $?)
    echo "diff_status=$diff_status" >> "$GITHUB_OUTPUT"
    if [ $diff_status -ne 0 ]; then
      git config user.name "GitHub Action"
      git config user.email "action@github.com"
      git add .
      git commit --message "chore: フォーマット [AUTO]"
      git push
    fi

Actionsからのコミットに利用するemailはなにがいいんだ、という議論があります[2]が、 action@github.com だとアイコンが表示されてイイカンジという感じです。

完了の通知

成否によってコメントで教えてあげるとわかりやすいでしょう。

  • 失敗した場合: if: failure() && steps.main.outcome == 'failure'
  • 成功した場合: if: success() && steps.main.outputs.diff_status != '0'
  • フォーマットの差分がなかった場合: if: success() && steps.main.outputs.diff_status == '0'

詳細な例はフルバージョンを参照してください。

peter-evans/create-or-update-comment を利用すれば、コメントをすることが可能です。

@${{ github.event.comment.user.login }} とかけば、コメントした人へのメンションになります。

uses: peter-evans/create-or-update-comment@v4
with:
  issue-number: ${{ github.event.issue.number }}
  body: |
    @${{ github.event.comment.user.login }}
    ⏹フォーマットの差分がありませんでした。
    詳細は[Actionsのログ](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})のジョブ`${{ github.job }}`を確認してください。
    通知タグ: CMD_NOTIFY_ANY, CMD_NOTIFY_FORMAT, CMD_NOTIFY_FORMAT_SKIP

通知タグは、メールなどで通知をフィルタしやすいように私のチームでは必ず付けています。

これから始まるChatOps

さて、ChatOpsという言葉を聞いたことがありますでしょうか。社内でも使ってる人を見かけないので、広まれば良いなと思っています。チャットでオペレーションしよう、そういうことです。

SlackでもDenoを使って簡単にワークフローが書けますし、今回紹介させていただいたようなワークフローを書けば、簡単に入門できると思います。
これは運用というほどのことをする例ではないかもしれませんが、こういう簡単なところから始めるChatOpsもいいのではないでしょうか。
ぜひ実践してみてください。

脚注
  1. https://docs.github.com/ja/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#issue_comment ↩︎

  2. https://qiita.com/thaim/items/3d1a4d09ec4a7d8844ce などを参照 ↩︎

GitHubで編集を提案
OPTIMINDテックブログ

Discussion