CircleCI self-hosted runner(macOS)でnpmやnodeが見つからない問題を解決する

CircleCI self-hosted runner(macOS)でnpmやnodeが見つからない問題を解決する
この記事の操作
Markdownで見る

Chrome(最新版)のBuilt-in AIが必要です。

Chrome(最新版)のBuilt-in AIが必要です。

CircleCI の self-hosted runner を macOS にセットアップして、いざジョブを実行してみたら npm: command not found というエラーに遭遇しました。ローカルのターミナルでは問題なく動作するのに、なぜかCIでは見つからない。この記事では、この問題の原因と解決方法について解説します。

遭遇した問題

macOSにCircleCIのmachine runner 3をセットアップし、以下のような簡単なジョブを実行しました。

version: 2.1

jobs:
  test:
    machine: true
    resource_class: my-namespace/my-macos-runner
    steps:
      - checkout
      - run:
          name: Run npm install
          command: npm install

しかし、ジョブは以下のエラーで失敗しました。

/bin/bash: line 1: npm: command not found
Exited with code exit status 127

ローカルのターミナルで which npm を実行すると、/Users/username/.fnm/node-versions/v20.11.0/installation/bin/npm のようにパスが表示されます。コマンドは実行できる、パスも通っている、しかしself-hosted runnerでは動作しない。このような不思議な状況が発生していました。

self-hosted runnerは非ログインシェルでコマンドを実行する

色々調べた結果、CircleCIのジョブとしてコマンドを実行する場合、非ログインシェルとして実行される仕様が原因であることがわかりました。ログイン中のユーザーとしてではなく、非ログインシェルでコマンドが実行されるため、ユーザーディレクトリにある~/.zshrc や ~/.bash_profile が読み込まれません。そのため、fnmやrbenvなどで設定したパスが通っていない状態にてジョブが実行される事になります。

この問題は、以下のようなバージョン管理ツールを使用している場合に発生します。

  • Node.js: fnm, nvm, volta
  • Ruby: rbenv, rvm
  • Python: pyenv
  • その他: asdf など

解決策1: $BASH_ENVを使ってPATHを設定する

最もシンプルな解決策は、ジョブ内で明示的にPATHを設定することです。CircleCIは各stepの開始時に$BASH_ENVsourceするため、ここに設定を書き込むと後続のstepでも有効になります。

jobs:
  test:
    machine: true
    resource_class: my-namespace/my-macos-runner
    steps:
      - checkout
      
      # Node.js / npm の初期化(fnmの例)
      - run:
          name: Initialize Node.js environment
          command: |
            set -euo pipefail
            
            # fnmのパスを設定
            if [ -d "$HOME/.fnm" ]; then
              echo 'export PATH="$HOME/.fnm:$PATH"' >> "$BASH_ENV"
            fi
            
            # fnmの初期化
            echo 'if command -v fnm >/dev/null 2>&1; then eval "$(fnm env --shell bash)"; fi' >> "$BASH_ENV"
            
            # この step 内でも有効化
            source "$BASH_ENV"
            
            # 確認(デバッグ用)
            echo "[debug] PATH=$PATH"
            node -v
            npm -v
      
      # 以降のstepでnpmが使える
      - run:
          name: Install dependencies
          command: npm ci
      
      - run:
          name: Run tests
          command: npm test

解決策2: runner.command_prefixを使う(より根本的な対処)

より根本的な解決策として、runner設定でcommand_prefixを使用する方法があります。これにより、すべてのジョブ実行前に自動的に環境を初期化できます。

1. ラッパースクリプトを作成

まず、環境変数を初期化するラッパースクリプトを作成します。

# /opt/circleci/runner-wrapper.sh
#!/bin/bash
set -euo pipefail

echo "=== Runner Wrapper: Initializing environment ==="

# fnmの初期化
if [ -d "$HOME/.fnm" ]; then
  export PATH="$HOME/.fnm:$PATH"
  if command -v fnm >/dev/null 2>&1; then
    eval "$(fnm env --shell bash)"
  fi
fi

# rbenvの初期化(必要な場合)
if [ -d "$HOME/.rbenv" ]; then
  export PATH="$HOME/.rbenv/bin:$PATH"
  eval "$(rbenv init - bash)"
fi

# 環境変数の確認(デバッグ用)
echo "[debug] PATH=$PATH"
if command -v node >/dev/null 2>&1; then
  echo "[debug] Node: $(node -v)"
fi
if command -v ruby >/dev/null 2>&1; then
  echo "[debug] Ruby: $(ruby -v)"
fi

echo "=== Running CircleCI task agent ==="

# task-agentを実行
task_agent_cmd=${@:1}
$task_agent_cmd
exit_code=$?

echo "=== CircleCI task agent finished with exit code: $exit_code ==="
exit $exit_code

実行権限を付与します。

chmod +x /opt/circleci/runner-wrapper.sh

2. runner設定を更新

config.yamlcommand_prefixを追加します。

runner:
  name: "my-macos-runner"
  working_directory: "/Users/$USER/Library/com.circleci.runner/workdir"
  cleanup_working_directory: true
  command_prefix: ["/opt/circleci/runner-wrapper.sh"]
api:
  auth_token: "your-auth-token"

3. runnerを再起動

launchctl disable gui/$(id -u)/com.circleci.runner
launchctl bootout gui/$(id -u)/com.circleci.runner
launchctl bootstrap gui/$(id -u) $HOME/Library/LaunchAgents/com.circleci.runner.plist
launchctl enable gui/$(id -u)/com.circleci.runner
launchctl kickstart -k gui/$(id -u)/com.circleci.runner

この方法の利点は、すべてのジョブで自動的に環境が初期化されるため、個別のジョブで初期化stepを追加する必要がなくなることです。

Homebrewでインストールしたツールの場合

Homebrewでインストールしたツール(例:brew install node)の場合は、比較的シンプルです。

- run:
    name: Setup Homebrew PATH
    command: |
      echo 'export PATH="/opt/homebrew/bin:$PATH"' >> "$BASH_ENV"
      source "$BASH_ENV"

Homebrewのパスは通常固定されているため、バージョン管理ツールよりも扱いやすいです。

まとめ

CircleCI の macOS self-hosted runner で npm や node が見つからない問題は、非ログインシェルでの実行が原因です。解決策としては以下の2つがあります。

  1. $BASH_ENVを使った方法:ジョブ内で明示的にPATHを設定
  2. runner.command_prefixを使った方法:ラッパースクリプトで一括初期化

どちらの方法も有効ですが、使用状況に応じて選択しましょう。個人的には、複数のプロジェクトで使う場合はrunner.command_prefix、特定のプロジェクトのみの場合は$BASH_ENVを推奨します。

この問題は、他のCI/CDサービスのself-hosted runnerでも発生する可能性があるため、基本的な仕組みを理解しておくと応用が効きます。

参考情報

シェア:

Hidetaka Okamoto profile photo

Hidetaka Okamoto

ビジネスデベロップメント

CircleCIシニアフィールドエンジニア。AWSやCloudflare上へのサーバーレスなアプリ開発を得意とする開発者。元Stripe Developer Advocate / AWS Samurai 2017など、サービスの使い方や活用Tipsを紹介するコンテンツ作成や登壇などを得意とする。

⭐ この記事への反応

はてなアカウントでスターを付けることができます

関連記事

CI / CDの設定を共有可能にする CircleCI URL orbsの始め方

この記事では、 CircleCI の CI / CD 設定を複数プロジェクトで再利用できる形として集約管理するための「CircleCI URL Orb」について紹介します。 1つの開発チームが複数のプロジェクトを運用して […]

Cursor x CircleCI MCPサーバーで CI パイプラインの分析やコスト最適化を実施する

*この記事は、Cursor Advent Calendar 2025の記事です。 開発チームにとって、開発フローやツールのコスト最適化は定期的に見直しや取り組みが必要なタスクの1つです。プロダクト・事業者目線においても、 […]

VS Code 拡張機能を利用して、Git Push なしで CircleCI パイプラインをテストする

この記事では、 CircleCI を利用して CI / CD パイプラインを構築する際の設定変更を簡単にテストする方法。特にGitを使わずにパイプラインを実行する方法について紹介します。この記事を読むことで、 Circl […]

CircleCIでコーディングエージェントにCIエラーの修正指示を出す

AIコーディングにおいて、自動テストやCIサービスによる品質チェックは欠かすことのできない要件です。実行するたびに生成結果が変わる生成AIには、意図しない設計や実装・変更などが紛れ込むリスクがあり、それを回避するための安 […]