Claude Media
Claude Codeでreusable workflowとcomposite actionに重複手順を切り出す

Claude Codeでreusable workflowとcomposite actionに重複手順を切り出す

3つのworkflowに散らばる同じ手順を、Claude Codeで洗い出してreusable workflowとcomposite actionへ切り出します。inputsとsecretsの切り方からactionlint検証までです。

同じ手順が複数のworkflowにコピーされているなら、切り出す先は2つあります。stepの並びだけならcomposite action、jobごとならreusable workflowです。Claude Codeには「どの手順が重複しているか」の洗い出しと、書き換え後の検証までを任せられます。

この記事では、CI用・レビュー用・定期レポート用の3つのworkflowを例に、重複の洗い出しから切り出し、workflow_call の inputs と secrets の切り方、呼び出し側の差し替え、actionlintでの検証までを順に進めます。例のYAMLは、公式ドキュメントの記述に沿って組んだ説明用の構成です。

まず決める: composite actionとreusable workflowのどちらにするか

2つの機能は、重複を減らすという目的は同じでも、切り出せる単位が違います。GitHub Docsの比較表から、切り出し先を決める材料になる行を抜き出します。

観点reusable workflowcomposite action
置き場所reusable workflow.github/workflows 直下の1ファイルcomposite actionaction.yml を含むディレクトリかリポジトリ
呼び出し方reusable workflowjob直下の uses:composite actionjob内のstepの uses:
含められるものreusable workflow複数のjobとstepcomposite actionstepのみ(jobは含められない)
ログreusable workflowstepごとに記録されるcomposite action呼び出し側の1stepとして表示される
secretsreusable workflow使えるcomposite action比較表では「使えない」
ネストreusable workflow最大10階層composite action1つのworkflowで最大10個まで
マーケットプレイスreusable workflow公開できないcomposite action公開できる

見分けるときの要点は、runnerとstepの前後関係です。切り出したい処理が別のマシンで動く必要があるなら、reusable workflowを選びます。呼び出した後に同じjobで別のstepを続けたいなら、composite actionです。reusable workflowの呼び出しはjob直下に書くため、呼んだ後にstepを足せません。

切り分け

切り出し先の切り分け

  1. Q切り出したいのは、jobの中のstepの並びだけですか?
    答えがいいえのとき

    複数のjobや、呼び出し側と別のrunnerが必要なら、reusable workflowにします。

    答えがはい

stepの並びだけなら composite action

.github/actions/<名前>/action.yml に置き、各workflowのstepから uses: で呼びます。

この記事の例では両方を使います。「Node.jsを入れて依存関係を入れる」というstepの並びをcomposite actionに、「Claude Code GitHub Actionを動かすjob」をreusable workflowに切り出します。

Claude Codeに重複を洗い出させる

いきなり書き換えさせず、先に洗い出しだけを頼みます。切り出しの設計を人が見てから、編集に進めるためです。

.github/workflows/ 配下の全workflowを読んで、複数のファイルに重複している
stepとjobの設定を洗い出してください。まだファイルは編集しないでください。
 
- 重複ごとに、出現するファイルと行を一覧にする
- 重複がstepの並びだけか、jobの設定(runs-on、permissions、timeout)まで
  含むかを分けて書く
- それぞれ、composite actionとreusable workflowのどちらが向くかを
  理由つきで提案する
- 値だけが違う箇所は、inputsにすべき候補として挙げる

返ってきた一覧では、特に「値だけが違う箇所」を確認します。そこが inputs の候補になり、切り出し後の呼び出し側に残る差分になります。例えば次の3つのworkflowは、どれも同じ土台の上で、プロンプトとツール許可だけが違います。

workflow目的共通の手順違う部分
ci.yml目的PRでテストを走らせる共通の手順checkout、Node.jsの準備、npm ci違う部分テストのコマンド
claude-review.yml目的PRにレビューを付ける共通の手順checkout、(依存関係)、claude-code-action違う部分プロンプトとツール許可
claude-report.yml目的毎朝の状況レポート共通の手順claude-code-action違う部分プロンプトとツール許可

ここから、Node.jsと依存関係の手順をcomposite action、Claude Code GitHub Actionを動かすjobをreusable workflowにする方針が立ちます。

CLAUDE.mdに規約を書いておくと、次回以降の新しいworkflowでも同じ方針で書かれます。

## GitHub Actionsのworkflow
 
- 3つ目の重複が出たら、コピーせず切り出す。stepの並びは
  `.github/actions/` のcomposite action、jobごとなら
  `.github/workflows/` のreusable workflowにする
- composite actionの入力は、`run:` に直接展開せず `env:` 経由で渡す
- secretsはreusable workflowの `on.workflow_call.secrets` に宣言して
  名前で渡す。composite actionにはinputsとして渡す
- 変更のたびに `actionlint` を通す

composite actionに切り出す

Node.jsの準備と依存関係のインストールを、.github/actions/setup-node-deps/action.yml に移します。GitHub Docsのチュートリアルは、同じリポジトリ内に置くなら .github/actions 配下を勧めています。

name: Setup Node dependencies
description: Node.jsを準備して依存関係をインストールする
inputs:
  node-version:
    description: 使うNode.jsのバージョン
    required: false
    default: "22"
runs:
  using: composite
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: npm
    - name: Install dependencies
      run: npm ci
      shell: bash

押さえておく点は3つです。

  • runs.using を composite にし、stepを runs.steps に並べる
  • run: を使うstepには shell: を書く。チュートリアルの例では、run: のstepすべてに shell: bash が付いている
  • 値を受け取る入口は inputs で、中からは ${{ inputs.node-version }} で参照する

入力をシェルのコマンドに使う場合は、run: に ${{ inputs.xxx }} を直接書かず、env: で環境変数に渡してから参照します。チュートリアルの例もそうしていて、攻撃者が入力に悪意ある内容を仕込む可能性への注意書きが付いています。

呼び出し側のworkflowは、actions/checkout の後にローカルパスで指定します。

name: CI
on:
  pull_request:
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: ./.github/actions/setup-node-deps
        with:
          node-version: "22"
      - run: npm test

action.yml はリポジトリ内にあるので、checkoutの後でなければ見つかりません。チュートリアルの呼び出し例も、checkoutの次に uses: ./.github/actions/... を置いています。composite actionは1つのstepとして動くため、呼び出しの前後に自分のstep(ここでは npm test)を足せます。

reusable workflowに切り出す

次に、Claude Code GitHub Actionを動かすjobを .github/workflows/claude-run.yml に移します。claude-code-actionの設定のうち、workflowごとに変わる値は2つです。プロンプトと、claude_args に渡すツール許可などの引数です。この2つを inputs にし、APIキーを secrets にします。

name: Claude run
on:
  workflow_call:
    inputs:
      prompt:
        description: Claudeへの指示
        required: true
        type: string
      claude-args:
        description: claude_argsに渡すCLI引数
        required: false
        type: string
        default: "--max-turns 10"
      install-deps:
        description: Claudeの実行前に依存関係を入れるか
        required: false
        type: boolean
        default: false
    secrets:
      anthropic_api_key:
        required: true
 
jobs:
  claude:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 1
      - if: inputs.install-deps
        uses: ./.github/actions/setup-node-deps
      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.anthropic_api_key }}
          prompt: ${{ inputs.prompt }}
          claude_args: ${{ inputs.claude-args }}

on.workflow_call に inputs と secrets を宣言し、jobの中から inputs.* と secrets.* で参照します。これがGitHub Docsの説明する3段階のうちの前半2つで、残る1つは呼び出し側が with: と secrets: で渡すことです。入力値の型は、呼び出される側の type(boolean、number、string)と一致させます。

permissions は、あえてこのファイルに書いていません。claude-code-actionのデフォルトのGitHub App認証には id-token: write が要り、レビューならPRへの書き込みも要ります。必要な権限は用途で変わるので、呼び出し側のjobで決める形にします。GitHub Docsの呼び出し例も、呼び出し側のjobに permissions を書いています。

呼び出し側は、数行になります。

name: Claude review
on:
  pull_request:
    types: [opened, synchronize, ready_for_review, reopened]
 
jobs:
  review:
    permissions:
      contents: read
      pull-requests: write
      issues: read
      id-token: write
    uses: ./.github/workflows/claude-run.yml
    with:
      prompt: "Review pull request ${{ github.repository }}/pull/${{ github.event.pull_request.number }} and post the findings as inline comments"
      claude-args: '--max-turns 8 --allowedTools "mcp__github_inline_comment__create_inline_comment"'
      install-deps: true
    secrets:
      anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

毎朝のレポートも同じ形です。

name: Claude report
on:
  schedule:
    - cron: "0 9 * * *"
 
jobs:
  report:
    permissions:
      contents: read
      issues: read
      id-token: write
    uses: ./.github/workflows/claude-run.yml
    with:
      prompt: "Generate a summary of yesterday's commits and open issues"
      claude-args: '--allowedTools "mcp__github__list_commits,mcp__github__list_issues"'
    secrets:
      anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

ツール許可は、Claude Code GitHub Actionsのページの例にあるものを使っています。プレーンテキストのpromptだけを渡すと、claude_args の --allowedTools などで許可するまで、シェルもGitHub APIも使えません。

呼び出し側のjobに書けるキーは限られる

呼び出し側のjobでは runs-on が使えません。使うrunnerは呼ばれる側のjobが決めます。actionlintのドキュメントによると、uses: で呼ぶjobに使えるキーは name、uses、with、secrets、needs、if、permissions です。

そのため、元のworkflowに runs-on や steps が残っていたら、切り出しの途中でjobの片方に混在しています。actionlintは構文エラーとして報告します。

secretsの渡し方は3通り

APIキーの渡し方は状況で選びます。

渡し方書き方向く場面
名前で渡す書き方secrets: の下に anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}向く場面呼ばれる側が必要とするsecretが決まっている
まとめて渡す書き方secrets: inherit向く場面同じorganizationまたはenterprise内の呼び出し
環境のsecret書き方呼ばれる側のjobに environment を設定し、呼び出し側が名前で渡す向く場面環境ごとに値が違う

名前で渡す場合、左辺が呼ばれる側での名前、右辺が呼び出し側のsecretです。

落とし穴

環境のsecretを渡し忘れても、エラーにならず空文字になる

呼び出し側が環境のsecretを渡さないと、reusable workflowの中では空文字として解決され、実行はエラーを出しません。on.workflow_call.secrets に required: true を付ければ、渡し忘れた実行は失敗します。ただし、この設定が確かめるのは「渡されたか」だけで、値が空かどうかまでは見ません。

複数の階層で渡すsecretsは、直接の呼び出しにしか届かない

reusable workflowから別のreusable workflowを呼ぶ場合、secretsは直接呼ばれたworkflowにしか渡りません。A→B→Cという連鎖で、CがAのsecretを使うには、AからB、BからCへと順に渡します。権限は連鎖の途中で引き上げられず、維持か縮小だけです。共通のjobを何段にも重ねるより、1段に収めたほうが追いやすくなります。

呼び出し先の指定

同じリポジトリ内のreusable workflowは ./.github/workflows/<ファイル名> で呼べます。この形だと、呼ばれるworkflowは呼び出し側と同じコミットのものになります。別のリポジトリのworkflowを呼ぶなら owner/repo/.github/workflows/<ファイル名>@<ref> で、ref にはSHA、リリースタグ、ブランチ名を使えます。SHAが安定性と安全性の面で最も安全な指定とされています。

リポジトリをまたぐ場合の注意が1つあります。呼ばれる側のworkflowが actions/checkout を使うと、取得されるのは呼ばれる側ではなく、呼び出し側のリポジトリです。そのため、先の claude-run.yml が ./.github/actions/setup-node-deps を見に行く構成は、composite actionが呼び出し側にあるときだけ成立します。共通のworkflowを別リポジトリに置く場合は、composite actionも owner/repo/path@ref の形で指定します。複数のリポジトリへの配布は、GitHub Actionsのorganization展開で扱っています。

切り出した結果を、actionlintで検証する

切り出しで壊れやすいのは、名前の食い違いです。inputs の綴り、secrets の名前、outputs の対応は、YAMLとしては正しくても、実行して初めて空になって気づく類のミスです。actionlintはこの種の食い違いを、実行前に静的に拾います。導入とhookの設定はClaude Codeのactionlint活用にまとめています。

actionlintが切り出し後のファイルに対して見る項目は、ドキュメント上で次のとおりです。

対象検査内容
reusable workflowの定義検査内容inputs の type と default の整合、required との矛盾
reusable workflowの中身検査内容inputs.* secrets.* が宣言済みのキーか
reusable workflowの出力検査内容outputs の value が参照するjobの出力が存在するか
呼び出し側検査内容with で渡す入力が定義と合うか、使えないキー(runs-on など)がないか
ローカルのcomposite action検査内容必須の入力の渡し忘れと、未定義の入力
action.yml検査内容メタデータの構文

ドキュメントの出力例から、典型的な指摘を2件引きます。1件目は、inputs.url を inputs.uri と書き間違えた場合です。

test.yaml:20:23: property "uri" is not defined in object type {lucky_number: number; url: string} [expression]

2件目は、ローカルのcomposite actionに必須の入力を渡し忘れた場合です。

test.yaml:7:15: missing input "message" which is required by action "My action" defined at "./.github/actions/my-action". all required inputs are "message" [action]

どちらも行と列が付くので、Claude Codeは直す場所を探さずに済みます。

secretsの検査は、書き方で変わる

actionlintは、reusable workflowが on.workflow_call の下に secrets: を持たない場合、呼び出し側からsecretsが継承されると仮定します。この場合、secrets.FOO のような参照はエラーになりません。

一方、secrets: の宣言がある場合は、そこに無いsecretの参照が「未定義」として報告されます。つまり、secrets: を宣言しておくと、名前のtypoをactionlintが拾えます。claude-run.yml で anthropic_api_key を明示的に宣言したのは、このためでもあります。

検査が効いているか、わざと壊して確かめる

切り出し後に「指摘ゼロ」だったとしても、検査が働いているとは限りません。Claude Codeに、次のように頼みます。

claude-run.yml の ${{ inputs.prompt }} を ${{ inputs.promt }} に変えて
actionlint を実行し、指摘が出ることを確認してください。確認できたら
元に戻して、もう一度 actionlint が通ることを確認してください。

指摘が出なければ、hookやパスの設定が対象のファイルを拾っていません。次の項目で、その典型を扱います。

hookが見ているのは .github/workflows/ だけ

リンク先の記事で組んだPostToolUse hookは、編集されたファイルが .github/workflows/ 配下のYAMLのときだけactionlintを呼ぶ作りです。composite actionの action.yml は .github/actions/ に置くので、この条件に当たらず、編集しても検査されません。

actionlintのドキュメントによると、action.yml の検査は、workflowの uses: から参照されたものを対象にします。action.yml 単体を引数に渡すのではなく、変更時にリポジトリ全体を検査し直す形が合います。hookのスクリプトを、次のように直します。

#!/bin/bash
FILE=$(jq -r '.tool_input.file_path // empty')
case "$FILE" in
  */.github/workflows/*.yml|*/.github/workflows/*.yaml) TARGET="$FILE" ;;
  */.github/actions/*/action.yml|*/.github/actions/*/action.yaml) TARGET="" ;;
  *) exit 0 ;;
esac
cd "$CLAUDE_PROJECT_DIR" || exit 0
OUT=$(actionlint $TARGET 2>&1)
if [ $? -ne 0 ]; then
  echo "$OUT" >&2
  exit 2
fi

workflowの編集では従来どおりそのファイルだけを、action.yml の編集ではリポジトリ内の全workflowを検査します。$TARGET を引用符で囲まないのは、空のときに引数なしで実行するためです。

切り出しが済んだ後に確認すること

actionlintは静的な検査なので、実行時の挙動までは保証しません。切り出したworkflowを実際に動かして、次の点を見ます。

  • reusable workflowのログには、jobとstepが個別に出ます。composite actionの中身は、呼び出し側では1つのstepにまとまって表示されます。失敗したときは、composite actionのstepを開いて原因を探します
  • claude-code-actionの github_token を共通のworkflowに足さないようにします。Claude Code GitHub Actionsのトラブルシュートでは、デフォルトの GITHUB_TOKEN で作られたコミットではCIが起動しないため、github_token: ${{ secrets.GITHUB_TOKEN }} を渡している場合は外すよう案内されています。切り出しのついでに「念のため」で追加すると、Claudeのコミットに対してCIが動かなくなります
  • reusable workflowが結果を返す場合は、on.workflow_call.outputs にjobの出力を対応づけます。stepの出力を直接は指せず、いったんjobの outputs に写す必要があります

まとめ

重複を見つけたら、まず「stepだけか、jobごとか」を分けます。stepの並びだけならcomposite action、jobごとならreusable workflowです。値の違う箇所を inputs に、認証情報を secrets に寄せれば、呼び出し側は数行の差分になります。

切り出しの安全性は、洗い出しを先に、検証を後に置く順序で支えます。Claude Codeには編集の前に一覧を出させ、編集のたびにactionlintを通し、最後に入力名をわざと壊して検査が働くことを確かめてください。生成直後のYAMLを人が読み合わせる手順は、GitHub Actionsのワークフロー作成をClaude Codeで進める手順にあります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/github-actions
  2. docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows
  3. docs.github.com/en/actions/concepts/workflows-and-actions/reusing-workflow…
  4. docs.github.com/en/actions/tutorials/create-actions/create-a-composite-act…
  5. github.com/rhysd/actionlint/blob/main/docs/checks.md
この記事を共有:XはてブLinkedIn