PluginをCIから呼び出す:Claude Code Pluginの一歩先の使い方

こんにちは。

ファインディ株式会社でテックリードマネージャーをやらせてもらっている戸田です。

Claude CodeのPluginを使うと、社内で育てたSkillやAgentを、組織のメンバーにまとめて配布できるようになります。ファインディでも、この仕組みでセルフレビュー用のSkillを開発組織全体に配り、各自がPull request作成前に呼び出す形で活用してきました。

この記事では、そこからさらに一歩踏み込んだ使い方として、PluginをGitHub Actionsから呼び出してCIで動かす取り組みを紹介します。具体的には、Pluginに含まれるセルフレビューSkillをCIから定期実行し、指摘内容を反映したPull requestを自動で生成する仕組みです。

Pluginの使い方というと「Skillを社内で共有して、各自がローカルで叩く」という一面が語られがちですが、同じPluginをCI基盤から起動するという選択肢をとると、Pluginで育てたSkillが個人の開発体験だけでなく、チーム全体のプロセスにも効いてきます。

PluginとセルフレビューのSkillについては次の記事を参照してください。

tech.findy.co.jp

tech.findy.co.jp

Pluginで配って各自に使ってもらう運用

まず、ファインディでのセルフレビューの流れを整理します。

社内の開発用Pluginには、セルフレビューSkillが含まれていて、Pull request作成前にローカルでPull request作成用のコマンドから呼び出されます。Pluginとして配布しているので、新しくジョインしたメンバーも、追加の手順なしに同じSkillを使える状態になっています。

このあたりの「Pluginで開発ナレッジを横展開する」話は、冒頭に貼った記事で詳しく紹介しているので、そちらをぜひご覧ください。

この運用は、個人の開発体験を底上げするうえで十分機能しています。この記事が公開された頃には実に1500個ものPull requestで実行されており、「最後の一押し」として働いてくれています。

PluginをCIから呼び出す

この運用をベースに、さらにもう一歩踏み込んで、同じPluginをCIからも呼び出してみるというのが本記事のテーマです。

PluginはSkillやSub Agentなどの集まりなので、Pluginを参照できる実行環境さえ作れば、人間の代わりにCIから呼び出すこともできます。普段ローカルで走っているものと同じSkillを、anthropics/claude-code-action経由でGitHub Actionsから起動すれば、Skillを再実装することなくCI化できるわけです。

github.com

本記事では、この考え方の一例として、セルフレビューSkillを定期実行し、指摘事項ごとに別々のPull requestを自動生成するWorkflowを取り上げます。対象にするのは、既存コードのうち長期間触られていないファイルです。人間のレビューでは目が届きにくい箇所に対して、機械がレビューすることで技術的負債の蓄積を抑えるというねらいがあります。

具体的に組み上げたWorkflowは、2つのjobで動きます。

  1. 指摘抽出 job:Pluginのセルフレビュー用Skillを呼び出し、対象コードに対する指摘を抽出する。
  2. 修正Pull request作成 job(matrix):指摘の件数を matrix strategy に展開し、指摘内容ごとに独立したjobを並列起動。各jobは新しい runner 上で指摘内容を1件だけ修正したのち、PluginのPull request作成SkillでPull requestを作成

結果として、朝出社すると改善Pull requestの候補が並んでいる状態になりました。ローカルで個人が手動実行するSkillと、CIから定期実行されるSkillが、同じPluginの中身を共有している——これが、Pluginの使い方をもう一段広げてくれるポイントでした。

この仕組み自体はセルフレビュー専用のものというより、「PluginをCIから起動する」というパターンの1つの応用です。同じ構造で、様々なSkillをCIに組み込めます。

実際のWorkflowを読み解く

まずは、今回組み上げたWorkflowの全体像です。PluginのSkillをCIから呼び出して指摘、Pull requestを並列生成するという流れを一通り確認できます。

name: Scheduled Self Review

on:
  schedule:
    - cron: '0 19 * * 0-4'  # 平日 JST 04:00(UTC 19:00 Sun-Thu)
  workflow_dispatch:

env:
  SELF_REVIEW_MAX_FINDINGS: "3"

jobs:
  # ---------------------------------------------------------------------------
  # Job 1: findings 抽出
  #   セルフレビューSkillで stale files をレビューし、
  #   指摘を findings.json として artifact 化。matrix 展開用の index も出力。
  # ---------------------------------------------------------------------------
  extract-findings:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    outputs:
      finding_indices: ${{ steps.compute-matrix.outputs.finding_indices }}
      findings_count: ${{ steps.compute-matrix.outputs.findings_count }}
    steps:
      - id: app-token
        uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2
        with:
          app-id: ${{ secrets.SELF_REVIEWER_APP_ID }}
          private-key: ${{ secrets.SELF_REVIEWER_APP_PRIVATE_KEY }}

      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          token: ${{ steps.app-token.outputs.token }}
          fetch-depth: 0

      - name: Compute stale file list
        run: |
          git log --since="1 month ago" --name-only --pretty=format: origin/main \
            | grep -v '^$' | sort -u > .stale-recent.txt
          git ls-files | sort > .stale-all.txt
          comm -23 .stale-all.txt .stale-recent.txt > .stale-files.txt

      - name: Clone plugin repository
        run: |
          git clone https://x-access-token:${{ secrets.PLUGIN_REPO_READ_TOKEN }}@github.com/your-org/plugin-repo.git /tmp/plugin-repo

      - name: Extract findings via self-reviewer
        uses: anthropics/claude-code-action@4e5d8b13ca281a6d163cdb287d8917b216e00d6f # v1.0.103
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          github_token: ${{ steps.app-token.outputs.token }}
          plugin_marketplaces: |
            /tmp/plugin-repo
          plugins: |
            example-dev-plugin@plugin-repo
          claude_args: >-
            --allowedTools Read
            --allowedTools Write
            --allowedTools "Skill(example-dev-plugin:<review-skill>)"
            --allowedTools "Skill(<review-skill>)"
            --allowedTools "Bash(cat:*)"
            --allowedTools "Bash(jq:*)"
            --allowedTools "Bash(gh pr list:*)"
          prompt: |
            .stale-files.txt に列挙された stale files を対象に、
            セルフレビューSkillを呼び出して指摘を抽出し、
            各指摘を 1 finding としてスキーマに従い findings.json に書き出せ。
            コード修正・Pull request作成は行わない(後続 matrix job が担当)。

      - id: compute-matrix
        run: |
          COUNT=$(jq length .self-review-findings.json)
          INDICES=$(jq -c 'to_entries | map(.key)' .self-review-findings.json)
          echo "findings_count=$COUNT" >> "$GITHUB_OUTPUT"
          echo "finding_indices=$INDICES" >> "$GITHUB_OUTPUT"

      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        if: steps.compute-matrix.outputs.findings_count != '0'
        with:
          name: self-review-findings
          path: .self-review-findings.json
          include-hidden-files: true

  # ---------------------------------------------------------------------------
  # Job 2: finding ごとに修正 + Pull request 作成(matrix で並列)
  #   各 matrix job は独立した runner で新規 checkout するため、
  #   worktree を使わずに済み、job 同士の編集衝突もそもそも発生しない。
  # ---------------------------------------------------------------------------
  fix-per-finding:
    needs: extract-findings
    if: needs.extract-findings.outputs.findings_count != '0'
    runs-on: ubuntu-latest
    timeout-minutes: 40
    strategy:
      fail-fast: false
      matrix:
        finding_index: ${{ fromJSON(needs.extract-findings.outputs.finding_indices) }}
    steps:
      - id: app-token
        uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2
        with:
          app-id: ${{ secrets.SELF_REVIEWER_APP_ID }}
          private-key: ${{ secrets.SELF_REVIEWER_APP_PRIVATE_KEY }}

      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          token: ${{ steps.app-token.outputs.token }}

      - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
        with:
          name: self-review-findings

      - name: Extract single finding
        id: finding
        run: |
          FINDING=$(jq -c ".[${{ matrix.finding_index }}]" .self-review-findings.json)
          {
            echo "finding<<EOF"
            echo "$FINDING"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"
          echo "branch=fix/self-review-$(date -u +%Y%m%d-%H%M)-${{ matrix.finding_index }}" >> "$GITHUB_OUTPUT"

      - name: Clone plugin repository
        run: |
          git clone https://x-access-token:${{ secrets.PLUGIN_REPO_READ_TOKEN }}@github.com/your-org/plugin-repo.git /tmp/plugin-repo

      - name: Fix finding and create Pull request
        uses: anthropics/claude-code-action@4e5d8b13ca281a6d163cdb287d8917b216e00d6f # v1.0.103
        env:
          FINDING_JSON: ${{ steps.finding.outputs.finding }}
          FINDING_BRANCH: ${{ steps.finding.outputs.branch }}
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          github_token: ${{ steps.app-token.outputs.token }}
          plugin_marketplaces: |
            /tmp/plugin-repo
          plugins: |
            example-dev-plugin@plugin-repo
          claude_args: >-
            --allowedTools Read
            --allowedTools Edit
            --allowedTools "Skill(example-dev-plugin:<pr-skill>)"
            --allowedTools "Skill(example-dev-plugin:<review-skill>)"
            --allowedTools "Skill(<pr-skill>)"
            --allowedTools "Skill(<review-skill>)"
            --allowedTools "Bash(git switch:*)"
            --allowedTools "Bash(git commit:*)"
            --allowedTools "Bash(git push:*)"
            --allowedTools "Bash(gh pr create:*)"
          prompt: |
            $FINDING_JSON が担当 finding、$FINDING_BRANCH が担当ブランチ。
            origin/main から $FINDING_BRANCH を切り、
            affected_locations のみを Edit → PR作成Skill で Pull request を作成。

ここからは、このWorkflowを設計するうえでポイントになった箇所を取り上げて解説していきます。

トリガーとスコープ

Pull requestではなくスケジュールをトリガーにしているのは、普段のPull requestで触る機会が少ないコードにも、同じSkillの観点を届けたいからです。

さらに対象ファイルは「直近1ヶ月以上変更されていないファイル(stale files)」に絞っています。最近の変更は通常のPull requestレビューで既にカバーされている前提で、それ以外の領域にフォーカスする設計です。

絞り込み自体は、git log --sinceで直近の変更ファイル一覧を作り、git ls-filescommで差分を取るだけのシンプルな処理です。AIにリポジトリを丸ごと渡すと、コンテキストも費用も膨らむので、「今効かせる価値がある範囲」に絞り込むための前処理を1段挟むのが運用上のコツでした。

PluginをCIに引き込む

Workflow内では、社内Plugin配布リポジトリを都度cloneし、Claude Code Actionのplugin_marketplacesに渡しています。

ここで効いているのが、Skillの実体をWorkflow側にコピーせず、Pluginからそのまま引き込むという構造です。Skillの改善はPlugin配布リポジトリにマージするだけで、自動的にすべてのリポジトリのCIに反映されます。ローカルで配っているSkillと同じものが、何のコピーもなしにCIで動く。これが、「Pluginで配るとローカルで使える」から「Pluginで配ったものがCIでも走る」への、ちょっとした発想の飛躍でした。

自動生成Pull requestにもCIを走らせるための GitHub App

地味に重要だったのが、github_tokenにGitHub Appのinstallation tokenを渡している部分です。

なぜGITHUB_TOKENではなくAppを使うのか。理由は、GITHUB_TOKENで作成されたPull requestはWorkflowを自動で発火させないというGitHub Actionsの仕様にあります。

自動生成されたPull requestに対しても、通常のPull requestと同じようにCIやセルフレビューのSkillを走らせたい。しかしGITHUB_TOKENで作るとこれらが自動で発火しません。App token経由のPull requestはこの制約の対象外なので、「AIが生成したPull requestにも、人間のPull requestと同じCIが自動で実行される」状態を作れます。

「PluginのSkillをCIから走らせる」を本気で運用に乗せるなら、token戦略はワンセットで考える必要がありました。

1指摘1Pull requestに切り出す

このWorkflowで考慮した設計判断の1つが、「見つかった複数の指摘を、1つのPull requestにまとめるか、1指摘1Pull requestに分けるか」です。

最終的に選んだのは、指摘事項ごとに別々のPull requestに切り出す方針でした。理由は次の通りです。

1つ目はレビュー観点の分離です。1つのPull requestに複数の無関係な改善が混ざると、レビュアーが見るべき観点がまばらになってしまいます。「このリファクタは妥当だが、あちらは慎重に見たい」といった判断を、Pull request単位で切り分けられる状態にしたいというのが背景でした。

2つ目はrevert単位の粒度です。生成AIによる特定の変更が本番で不具合を起こした場合を想定すると、他の変更を巻き込まずに戻せるような粒度に保つのが安全であると考えました。1指摘1Pull requestであれば、revertひとつで該当の変更だけ元に戻せます。

3つ目は依存関係なしにマージできることです。複数のPull requestを1本にまとめると、どれか1つがマージできないだけで全体がブロックされます。依存関係が少なくなるようにPull requestを分けておけば、マージできるものから順に処理できます。

代償として、レビュアーは「Pull requestの件数が増える」ことになります。ただし各Pull requestの変更量は小さく、観点も絞られているので、1本あたりのレビュー時間はむしろ短めになります。

GitHub Actionsのmatrixを使って並列でPull requestを作る

指摘ごとにPull requestを分ける前提に立つと、次は「どうやって並列で安全にPull requestを作るか」が論点になります。ここで採用したのが、GitHub Actionsの matrix strategy による job 並列化です。

具体的には、job を2つに分けています。

  1. 最初の job(extract-findings)でセルフレビューSkillを走らせ、指摘内容をまとめる
  2. 後続 job(fix-per-finding)は、その指摘内容ごとに matrix に展開する。matrix の要素数ぶん、独立した runner 上で並列に job が起動する

各matrix job は、指摘内容を1件だけ取り出して修正して、PluginのPull request作成SkillでPull requestを作ります。

並列化でありがちな事故は、編集の衝突です。複数のエージェントが同じファイルを同時に触ると、どちらかの変更が失われます。今回はこれを、matrix で分散した各 job が別 runner 上で独立に checkout するという構造で、そもそも発生しない形にしました。

strategy:
  fail-fast: false
  matrix:
    finding_index: ${{ fromJSON(needs.extract-findings.outputs.finding_indices) }}

fail-fast: false にしているのは、1つのmatrix jobの修正が失敗しても他の matrix job を止めないため。AIが生成する修正は当たり外れがあるので、「一部が失敗しても残りは流す」という姿勢で運用するほうが現実的でした。

この構造で効いているのが、Pluginとして配布されているSkillの存在です。extract-findings ではセルフレビューSkill、fix-per-finding ではPull request作成Skillをそのまま呼び出すだけで、「レビューする→修正する→Pull requestを作る」という流れが組み上がります。Pluginで配ったSkillが、CIの job 分割の単位とそのまま噛み合う。これが、「Pluginで配ってローカルで使う」の一歩先にある使い方です。

まとめ

Pluginで配ったSkillをGitHub Actionsにも載せたい場合に使えるポイントをまとめます。

  1. Plugin配布用リポジトリをWorkflow内でcloneし、plugin_marketplacesに渡す。Skillの改善はPluginリポジトリへのマージだけで、ローカルにもCIにも同じ内容が反映される
  2. 自動生成Pull requestにもCIを回したいなら、GITHUB_TOKENではなくGitHub App tokenを使う。発火チェーン抑制の対象外になる
  3. 並列で動かすなら、GitHub Actionsの matrix strategy で job を分離する。各 job は独立した runner で新規 checkout するので、編集衝突はそもそも発生しない

Pluginで配ったSkillは、個人の開発体験を底上げするだけでなく、CIに載せることでチーム全体のプロセスにも効いてくる使い方ができます。「ローカルで配って使う」の次の一歩として、参考になればうれしいです。


ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers