CircleCI self-hosted runner(macOS)でnpmやnodeが見つからない問題を解決する
この記事の操作
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_ENVをsourceするため、ここに設定を書き込むと後続の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.yamlにcommand_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つがあります。
$BASH_ENVを使った方法:ジョブ内で明示的にPATHを設定runner.command_prefixを使った方法:ラッパースクリプトで一括初期化
どちらの方法も有効ですが、使用状況に応じて選択しましょう。個人的には、複数のプロジェクトで使う場合はrunner.command_prefix、特定のプロジェクトのみの場合は$BASH_ENVを推奨します。
この問題は、他のCI/CDサービスのself-hosted runnerでも発生する可能性があるため、基本的な仕組みを理解しておくと応用が効きます。
参考情報
⭐ この記事への反応
はてなアカウントでスターを付けることができます
関連記事
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には、意図しない設計や実装・変更などが紛れ込むリスクがあり、それを回避するための安 […]
