ローカルでは成功するのにGitHub Actionsで失敗する原因|環境差の切り分け方

ローカルでは通る処理がGitHub Actionsだけで失敗するとき、Node.js、依存関係、OS、環境変数、権限、作業ディレクトリ、キャッシュの差を順に切り分けます。

ローカルでは成功するのにGitHub Actionsだけで失敗する場合、同じコードでも実行条件のどこかが異なります。再実行を繰り返すより、対象コミット、Node.js、依存関係、OS、環境変数、権限、開始位置を一つずつ比較する方が原因へ近づけます。

GitHubがホストする標準ランナーでは、原則としてジョブごとに新しい実行環境が用意されます。開発PCに残ったファイルや手動設定には頼れません。ここでは、差を証拠とともに狭める順番を説明します。

最初に対象と失敗点を固定する

Actionsの失敗したジョブを開き、赤くなったステップと最初のエラーを探します。最後の連鎖的なエラーだけでなく、その直前にある最初の失敗行を見ます。ローカル側も同じコミット、同じコマンドで比べます。

再実行のたびにコードや条件を変えると、何が効いたのか分からなくなります。GitHub公式資料によると、workflowの再実行では元のイベントと同じGITHUB_SHAGITHUB_REFが使われます。まず修正前の実行をそのまま再実行し、一時的な失敗か、同じ条件で再現する失敗かを分けます。

通常ログだけで原因を絞れない場合は、再実行画面の「Enable debug logging」を有効にできます。デバッグログには実行の詳細が増えるため、公開してよい情報だけが出ているかを確認し、ログを共有する前に秘密や個人情報が含まれていないか見直してください。

環境差の切り分け早見表

症状最初に疑う差比較する証拠安全な確認方法
npm ciが失敗package.jsonとlockfile、npm設定最初のnpmエラー、lockfileの対象コミット同じコミットでnpm ciを実行
ビルドで構文・APIエラーNode.jsやパッケージの版node --versionnpm --versionローカルとActionsで版を表示
ファイルが見つからない大文字・小文字、パス、開始位置runner.os、現在位置、対象ファイルの有無値や一覧を必要最小限だけ表示
認証処理だけ失敗環境変数、Secrets、権限変数の存在、permissions、イベント種別秘密の値ではなく存在だけ検査
実行のたびに成否が変わるキャッシュ、通信、状態依存cache hit、外部通信の応答、再実行結果キャッシュなしの診断実行と比較
ジョブやステップが飛ばされるif条件、ブランチ、イベント条件式の評価結果、github.refログアーカイブやデバッグログを確認

ログの実行コマンド、終了コード、ランナーOS、対象コミットを記録します。修正後はAIにコードを直させた後の回帰テストのように、同じ失敗手順を再実行します。

Node.jsと依存関係を比較する

開発PCとActionsの両方でnode --versionnpm --versionを確認します。actions/setup-nodeではnode-versionを指定でき、公式READMEもランナーのPATHへ偶然入っている版に頼らず、版を明示するよう勧めています。

- uses: actions/setup-node@v6
  with:
    node-version: 24
- run: |
    node --version
    npm --version
    npm ci

ローカル、package.jsonenginesや版指定ファイル、Actionsの指定を同じ条件へそろえます。詳しくはNode.jsのバージョン違いを直す方法も確認してください。

npm公式資料によると、npm ciはlockfileを必要とし、package.jsonと内容が一致しなければ更新せず失敗します。また、既存のnode_modulesを処理前に削除します。ローカルでnpm installだけが通るなら、未反映のlockfileや残存パッケージに助けられていないかを調べます。

OSとパスの違いを見る

開発PCがWindows、ランナーがubuntu-latestなら、シェル、パス、ファイル名の大文字・小文字、利用できるコマンドが異なります。参照がSrc/App.ts、実ファイルがsrc/App.tsなら表記を実物へ合わせます。PC固有の絶対パスもコードへ入れません。

環境変数の書式も、LinuxやmacOSのbash系は$NAME、WindowsのPowerShellは$env:NAMEです。切り分け時はOSと現在位置だけを安全にログへ出します。

- name: OSと開始位置を確認
  run: |
    echo "OS=${{ runner.os }}"
    node -e "console.log(process.cwd())"
    node -e "console.log(require('node:fs').existsSync('package.json'))"

環境変数と権限を確認する

ローカルの.envやシェル設定はActionsへ自動では移りません。秘密ではない設定はenvやVariables、APIキーなどはSecretsへ分けます。値を表示せず、存在だけを確認します。

- name: 必須変数を確認
  env:
    API_TOKEN: ${{ secrets.API_TOKEN }}
  run: node -e "if(!process.env.API_TOKEN) throw new Error('API_TOKEN is not set')"

GitHub ActionsではGITHUB_TOKENがジョブごとに作られ、その権限はpermissionsで絞れます。ローカルの個人トークンと同じ権限があるとは限りません。書き込みだけが失敗する場合は、必要な操作とpermissionsを照合し、調査目的で広い権限を一括付与しないでください。Secretsをechoしたり、環境全体を出力したりする確認も避けます。

作業ディレクトリをそろえる

複数パッケージを持つ構成では、ローカルはサブフォルダへ移動しているのに、Actionsはリポジトリ直下から始めていることがあります。GitHub公式資料では、defaults.run.working-directoryrunステップの既定位置を指定できます。

jobs:
  test:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./app
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: npm
          cache-dependency-path: app/package-lock.json
      - run: npm ci
      - run: npm test

package.json、lockfile、設定ファイルをどこから探すのかを確認し、ステップごとのcdworking-directoryを混在させないようにします。サブフォルダのlockfileをキャッシュに使う場合はcache-dependency-pathも合わせます。

キャッシュなしで再現する

キャッシュは取得時間を減らしますが、ビルドやテストを省略する仕組みではありません。GitHub公式資料では、完全一致、部分一致、restore-keysの順に候補を探します。広すぎる復元キーがあると、想定外のデータを再利用する可能性があります。

一時的にキャッシュ設定を外した診断実行と通常実行を比べます。setup-nodeのnpmキャッシュはnode_modulesそのものではなく、npmのパッケージデータが対象です。キャッシュなしでもnpm ciとテストは実行します。詳しい確認順はGitHub Actionsのキャッシュ入門を参照してください。

キャッシュなしでも失敗するなら別の環境差です。キャッシュなしだけ成功するなら、lockfileのハッシュとOSをキーへ含め、曖昧な復元キーを狭めます。共有キャッシュをすぐ削除せず、診断用の新しいキーで試すと戻しやすくなります。

差を固定して再発を防ぐ

原因が分かったら、人の記憶ではなく設定へ残します。Node.js版はsetup-nodeと版指定ファイルへ、依存関係はlockfileへ、開始位置はworkflowへ、必須環境変数は値を伏せた検査へ固定します。

基本の順番は、checkout、環境固定、クリーンインストール、ビルド、テストです。デプロイがある場合は、検品成功後だけ進む構成にします。全体像はGitHub Actionsで記事サイトを自動検品する手順も参考になります。

最後に、失敗したコミットと最初のエラーを記録し、ローカルとCIで同じコマンドを実行します。環境差を一つずつ比較し、キャッシュなしでも再現するか確かめ、原因を設定へ固定します。修正前に失敗した手順と周辺テストが通れば完了です。「CIだけ失敗する」状態は、クリーンな環境がローカルに隠れていた前提を見つけた結果でもあります。

よくある質問

GitHub Actionsを再実行するだけで直った場合、修正は不要ですか?

一時的な通信障害などの可能性はありますが、再発しないとは限りません。元の実行と再実行のログを比べ、外部通信、キャッシュ、時間依存処理のどこで差が出たかを記録します。同じ失敗が続く場合は、環境差の調査へ進みます。

ローカルでActionsと同じコマンドが通れば、環境は同じですか?

コマンドが同じでも、OS、Node.js、環境変数、権限、開始位置は異なる場合があります。対象コミットとコマンドに加え、runner.os、Node.jsとnpmの版、作業ディレクトリを比較してください。

Secretsの値を確認するため、ログへ表示してもよいですか?

表示しないでください。サンプルのように、値が存在するかだけを判定します。ログ共有時は、URL、ユーザー名、内部パスなど秘密以外の情報も必要以上に含めないようにします。

npm installは通るのにnpm ciが失敗するのはなぜですか?

npm cipackage.jsonとlockfileの一致を要求し、既存のnode_modulesも利用しません。ローカルに残った依存関係や、コミットされていないlockfileの更新でnpm installだけが通っていないか確認します。違いはnpm ciとnpm installの使い分けでも整理しています。

デバッグログは常に有効にするべきですか?

通常ログで足りない調査時だけ有効にするのが扱いやすい方法です。ログ量が増えるため、必要な実行で使い、共有前に機密情報が含まれていないか確認します。

一次情報の確認メモ

続き(結論と実データ)はnoteに置いています

ここでは手順のところまで書きました。実際に出た数字、うまくいかなかった条件、そのまま使える設定ファイルは、 note の記事にまとめてあります。

noteで続きを読む Xをフォローする