Claude Media
Claude CodeでGitHub Actionsを高速化 — 遅い原因の特定と3つの直し方

Claude CodeでGitHub Actionsを高速化 — 遅い原因の特定と3つの直し方

ghで取ったステップ別の所要時間をClaude Codeに渡し、依存取得の遅さ・無駄な起動・重複実行を分けて直させる手順です。actions/cache、pathsフィルタ、concurrencyの入れ方と落とし穴も扱います。

CIが遅い原因は3種類に分けて直す

GitHub ActionsのCIを速くする手段は、依存関係のキャッシュ、起動条件を絞るpathsフィルタ、重複実行を止めるconcurrencyの3つに分かれます。どれが効くかはワークフローごとに違います。先に計測してから直すと、効かない手を入れずに済みます。

Claude Codeの使いどころは、この計測と切り分けです。ghで取った実行履歴を渡し、どのステップが遅いかを読ませ、原因ごとに別々の修正を作らせます。

症状効く手段短くなるもの
install・取得系のステップが毎回長い効く手段actions/cache(またはsetup-*のキャッシュ)短くなるもの1回の実行時間
ドキュメントだけの変更でもCIが走る効く手段paths / paths-ignore短くなるもの実行の回数
連続pushで古い実行が最後まで走る効く手段concurrencyのcancel-in-progress短くなるもの無駄に消費する実行時間
テスト本体が長い効く手段この記事では扱わない(分割・並列化の領域)短くなるもの—

3つは効く場所が違うので、1つの修正に混ぜないほうが効果を測れます。

ghでステップ別の所要時間を取る

gh run view --json jobsは、実行に含まれるジョブとステップの情報をJSONで返します。公式マニュアルにはJSONフィールドとしてjobsが載っています。手元のgh 2.90.0で公開リポジトリの実行を見ると、ジョブとステップのそれぞれにstartedAtとcompletedAtが入っていました。

# 直近の成功した実行を5件取る
gh run list --workflow ci.yml --status success --limit 5 \
  --json databaseId,createdAt
 
# 1件のステップ別の所要時間(秒)を長い順に並べる
gh run view 1234567890 --json jobs -q '
  .jobs[] | .name as $job | .steps[] |
  {job: $job, step: .name,
   sec: ((.completedAt | fromdate) - (.startedAt | fromdate))}
' | jq -s 'sort_by(-.sec) | .[0:8]'

ci.ymlと実行IDは自分のリポジトリのものに置き換えます。ステップの所要時間は秒単位で、1秒未満のステップは0秒になります。

1回の実行だけでは、たまたま遅かった回に引きずられます。同じコマンドを3〜5回分まわし、ステップ名ごとに平均を取った表をClaude Codeに渡すと、判断がぶれにくくなります。

Claude Codeに渡す指示は「原因ごとに別の変更」にする

「CIを速くして」とだけ頼むと、キャッシュもフィルタもまとめて書き換える差分が返りがちです。次のように、計測結果と変更の単位を指定します。

.github/workflows/ci.yml を速くしたい。
添付の表は直近5回のステップ別の平均所要時間(秒)。
 
1. 上位3ステップが何をしているか、workflow から読み取って説明する
2. 次の3つに分類し、それぞれ別のコミットで直す
   - キャッシュで短くなるもの(依存の取得・ビルド成果物)
   - 起動条件で減らせるもの(paths / paths-ignore)
   - 重複実行を止められるもの(concurrency)
3. キャッシュのキーは lockfile のハッシュにし、何が変わると
   キャッシュが無効になるかを一文で書く
4. 効かないと判断したものは変更せず、理由だけ報告する

4番が肝です。分類して「何もしない」を選べるようにしておくと、効果のない設定が増えません。CLAUDE.mdに「ワークフローを変更するときは、変更前後のステップ別所要時間を比較する」と1行置いておけば、毎回の指示にも書かずに済みます。

キャッシュ: キーにlockfileのハッシュを入れる

依存の取得が遅いなら、まずキャッシュです。actions/cacheはキーが完全一致するとキャッシュを復元し、一致しなければジョブが成功した時点で新しいキャッシュを保存します。既存のキャッシュの中身は変更できないため、依存が変わったら新しいキーで作り直す設計になります。

公式の例は次の形です。キーにOSとlockfileのSHA-256ハッシュを入れ、package-lock.jsonが変わったときだけ新しいキャッシュを作ります。

- name: Cache node modules
  id: cache-npm
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-build-
      ${{ runner.os }}-

restore-keysは、完全一致が無いときの代替です。前方一致する中でいちばん新しいキャッシュが復元されるので、依存が少し変わっただけなら大半の取得を省けます。cache-hitの出力で完全一致かどうかも判定できます。

npm・Yarn・pnpmならsetup-nodeのcache入力でも足ります。setup-nodeはnode_modulesではなく、マシン全体のパッケージキャッシュを対象にします。lockfileが無いリポジトリでは、キャッシュ用のキーが作れないため使えません。

- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: 'npm'

setup-*の対応表では、pip・Poetry(setup-python)、Gradle・Maven(setup-java)、Goのgo.sum(setup-go)なども同じ方式です。

キャッシュが効かない・消える条件

「入れたのに速くならない」場合は、次の条件を疑います。

  • PRのキャッシュはそのPRの中だけ: pull_requestで作られたキャッシュはマージ参照(refs/pull/.../merge)に紐づき、PRの再実行からしか復元できません。ベースブランチや別のPRからは使えません
  • ベースブランチのpushでキャッシュを作る: 実行はまず現在のブランチ、次にデフォルトブランチのキャッシュを探します。mainへのpushで作ったキャッシュが、各PRの最初の実行を速くします
  • 7日間アクセスが無いと削除される: 更新頻度の低いリポジトリでは、週をまたぐと必ず初回扱いです
  • リポジトリあたり10GBが既定の上限: 超えると古いアクセス順に削除され、作成と削除が繰り返されることがあります
  • 低信頼のトリガーでは保存できない: pull_request_target、issue_comment、workflow_runで動く実行は、デフォルトブランチのキャッシュを読めても作れません。@claudeのコメントで動くClaudeのワークフローがキャッシュを作らないのは、この制限によるものです

キャッシュには、トークンなどの機密を置かない前提で設計します。PRを開ける人ならベースブランチのキャッシュを読めるためです。

cache-modeで読み取り専用にする

低信頼のトリガーで動くワークフローは、保存に失敗してもステップやジョブは失敗せず、ログに警告が出るだけです。警告を消して意図を明示するには、保存を伴わないactions/cache/restoreに切り替えます。キャッシュの更新は、デフォルトブランチへのpushで動く信頼できるワークフローに任せます。

ワークフローやジョブには、キャッシュへの権限を絞るcache-modeキーも指定できます。

値復元保存
read復元できる保存できない
write復元できる保存できる
write-only復元できない保存できる
none復元できない保存できない

省略時は、信頼できるトリガーならwrite、低信頼のトリガーならreadです。低信頼のトリガーでwriteを明示するとキャッシュ汚染のリスクが戻るため、Claudeに直させるときも「cache-modeは変えない」と条件に入れておくのが安全です。

setup-nodeには、package.jsonのpackageManagerフィールドがnpmのときに自動でキャッシュを有効にする挙動があります。lockfileを置かない構成では、package-manager-cache: falseで明示的に無効にします。

pathsフィルタ: ドキュメントだけの変更を除外する

実行の回数を減らすのがpathsとpaths-ignoreです。pushとpull_requestで使え、タグのpushには評価されません。

on:
  pull_request:
    paths-ignore:
      - 'docs/**'
      - '**.md'

除外したいものがある一方で、一部は残したいときはpathsに!を付けます。この場合は少なくとも1つ、!の無いパターンが必要です。同じイベントでpathsとpaths-ignoreは併用できません。パターンの順序にも意味があり、後ろの!が前の一致を打ち消し、さらに後ろの一致がもう一度含めます。

on:
  pull_request:
    paths:
      - 'src/**'
      - 'package-lock.json'
      - '.github/workflows/ci.yml'
      - '!src/**/*.md'

Claude Codeに書かせるときは、「テストが読むファイル」を洗い出させるのが近道です。src/だけを入れて、lockfileやワークフロー自身、設定ファイルを入れ忘れる修正が典型的な失敗です。

落とし穴

必須チェックが付いたPRは、スキップされるとマージできなくなる

パスフィルタなどでワークフローがスキップされると、そのワークフローのチェックは「Pending」のままです。ブランチ保護でそのチェックを必須にしていると、ドキュメントだけのPRがマージできなくなります。必須チェックにしているワークフローにはpathsを付けないか、ワークフロー内の条件でジョブだけを飛ばす構成にします。

変更ファイルの判定にも条件があります。1回のpushに1,000件を超えるコミットが含まれる場合や、差分の生成がタイムアウトした場合は、フィルタに関係なく必ず実行されます。差分が3,000ファイルを超え、フィルタに一致するファイルが先頭の3,000件に入らないと、逆に実行されません。

concurrency: 連続pushで古い実行を止める

同じPRに続けてpushすると、古い実行が最後まで走ります。concurrencyにcancel-in-progress: trueを付けると、同じグループの進行中の実行を止められます。

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

グループ名にgithub.workflowを入れるのが重要です。リポジトリ内の別のワークフローと同じグループ名にすると、無関係の実行まで取り消されます。cancel-in-progressは式も取れるので、リリースブランチだけ取り消さないこともできます。

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ !contains(github.ref, 'release/') }}

cancel-in-progressを付けない場合の既定の挙動も押さえておきます。同じグループで待機(pending)できる実行は1つで、新しい実行が来ると古い待機中の実行が取り消されます。進行中の実行は止まりません。デプロイのように途中で止めたくないジョブにはcancel-in-progressを付けず、PRの検証には付ける、という使い分けです。

Claude Code Actionのワークフローにも同じ考え方を当てる

anthropics/claude-code-actionを使うワークフローも、GitHubのランナーの分(Actionsの無料枠や課金対象)を消費します。公式のコスト管理の節は、実行時間とトークンの両方を抑える手段として、claude_argsの--max-turns、ワークフロー単位のタイムアウト、GitHubのconcurrency制御による並列実行の制限を挙げています。

concurrency:
  group: claude-${{ github.event.issue.number || github.event.pull_request.number }}
  cancel-in-progress: false
 
jobs:
  claude:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          claude_args: "--max-turns 8"

ここではcancel-in-progress: falseにして、進行中の実行を止めない形にしています。同じIssueやPRへのコメントは順に処理され、待機できるのは1件だけです。この設定値は一例で、公式の例ではありません。

CIとの関係でもう1点あります。Claudeのpushでテストが走らないときは、GITHUB_TOKENを渡していないか、CI側のonにpushやpull_requestが含まれているかを確かめます。pathsフィルタを絞りすぎると、Claudeがdocsだけを直したコミットでCIが走らないのは意図どおりですが、コードを直したのに走らない場合はパターンの漏れです。

直したあとに効果を測る

変更を入れたら、同じghコマンドで修正後の実行を取り、前後の平均を比べます。ステップ別の秒数の差を見るのはキャッシュ、実行回数の差を見るのはpaths、取り消された実行の数を見るのはconcurrencyです。

gh run list --workflow ci.yml --limit 20 \
  --json conclusion,createdAt,startedAt,updatedAt

conclusionがcancelledの実行はconcurrencyで止まったものを含みます。手動で止めた実行と区別がつかないため、数字は目安として読みます。

まとめ

CIの高速化は、計測、分類、1原因1変更の順に進めると効果が見えます。キャッシュは実行時間、pathsは実行回数、concurrencyは重複実行の無駄を減らすので、効果を混ぜずに測れます。必須チェックとの組み合わせでは、pathsだけが事故を起こします。ワークフローの新規作成から始める場合はワークフロー作成の手順を、ClaudeをActionsで動かす構成はClaude CodeのGitHub Actions組み込みを参照してください。パラメータの渡し方はclaude_argsの記事にあります。

参照した一次ソース

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

  1. code.claude.com/docs/en/github-actions
  2. docs.github.com/en/actions/reference/workflows-and-actions/dependency-cach…
  3. docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax
  4. cli.github.com/manual/gh_run_view
  5. github.com/actions/setup-node/blob/main/docs/advanced-usage.md
この記事を共有:XはてブLinkedIn