ローカルでは成功するのにGitHub Actionsで失敗する原因|環境差の切り分け方
ローカルでは通る処理がGitHub Actionsだけで失敗するとき、Node.js、依存関係、OS、環境変数、権限、作業ディレクトリ、キャッシュの差を順に切り分けます。
ローカルでは成功するのにGitHub Actionsだけで失敗する場合、同じコードでも実行条件のどこかが異なります。再実行を繰り返すより、対象コミット、Node.js、依存関係、OS、環境変数、権限、開始位置を一つずつ比較する方が原因へ近づけます。
GitHubがホストする標準ランナーでは、原則としてジョブごとに新しい実行環境が用意されます。開発PCに残ったファイルや手動設定には頼れません。ここでは、差を証拠とともに狭める順番を説明します。
最初に対象と失敗点を固定する
Actionsの失敗したジョブを開き、赤くなったステップと最初のエラーを探します。最後の連鎖的なエラーだけでなく、その直前にある最初の失敗行を見ます。ローカル側も同じコミット、同じコマンドで比べます。
再実行のたびにコードや条件を変えると、何が効いたのか分からなくなります。GitHub公式資料によると、workflowの再実行では元のイベントと同じGITHUB_SHAとGITHUB_REFが使われます。まず修正前の実行をそのまま再実行し、一時的な失敗か、同じ条件で再現する失敗かを分けます。
通常ログだけで原因を絞れない場合は、再実行画面の「Enable debug logging」を有効にできます。デバッグログには実行の詳細が増えるため、公開してよい情報だけが出ているかを確認し、ログを共有する前に秘密や個人情報が含まれていないか見直してください。
環境差の切り分け早見表
| 症状 | 最初に疑う差 | 比較する証拠 | 安全な確認方法 |
|---|---|---|---|
npm ciが失敗 | package.jsonとlockfile、npm設定 | 最初のnpmエラー、lockfileの対象コミット | 同じコミットでnpm ciを実行 |
| ビルドで構文・APIエラー | Node.jsやパッケージの版 | node --version、npm --version | ローカルとActionsで版を表示 |
| ファイルが見つからない | 大文字・小文字、パス、開始位置 | runner.os、現在位置、対象ファイルの有無 | 値や一覧を必要最小限だけ表示 |
| 認証処理だけ失敗 | 環境変数、Secrets、権限 | 変数の存在、permissions、イベント種別 | 秘密の値ではなく存在だけ検査 |
| 実行のたびに成否が変わる | キャッシュ、通信、状態依存 | cache hit、外部通信の応答、再実行結果 | キャッシュなしの診断実行と比較 |
| ジョブやステップが飛ばされる | if条件、ブランチ、イベント | 条件式の評価結果、github.ref | ログアーカイブやデバッグログを確認 |
ログの実行コマンド、終了コード、ランナーOS、対象コミットを記録します。修正後はAIにコードを直させた後の回帰テストのように、同じ失敗手順を再実行します。
Node.jsと依存関係を比較する
開発PCとActionsの両方でnode --versionとnpm --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.jsonのenginesや版指定ファイル、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-directoryでrunステップの既定位置を指定できます。
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、設定ファイルをどこから探すのかを確認し、ステップごとのcdとworking-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 ciはpackage.jsonとlockfileの一致を要求し、既存のnode_modulesも利用しません。ローカルに残った依存関係や、コミットされていないlockfileの更新でnpm installだけが通っていないか確認します。違いはnpm ciとnpm installの使い分けでも整理しています。
デバッグログは常に有効にするべきですか?
通常ログで足りない調査時だけ有効にするのが扱いやすい方法です。ログ量が増えるため、必要な実行で使い、共有前に機密情報が含まれていないか確認します。
一次情報の確認メモ
- 確認日: 2026-07-24 / GitHub Docs「GitHub-hosted runners reference」: https://docs.github.com/en/actions/reference/runners/github-hosted-runners
- 確認日: 2026-07-24 / GitHub Docs「Store information in variables」: https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-variables
- 確認日: 2026-07-24 / GitHub Docs「Setting a default shell and working directory」: https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/set-default-values-for-jobs
- 確認日: 2026-07-24 / GitHub Docs「Dependency caching reference」: https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching
- 確認日: 2026-07-24 / GitHub Docs「GITHUB_TOKEN」: https://docs.github.com/en/actions/concepts/security/github_token
- 確認日: 2026-07-24 / actions/setup-node公式README: https://github.com/actions/setup-node
- 確認日: 2026-07-24 / npm Docs「npm-ci」: https://docs.npmjs.com/cli/commands/npm-ci/
- 確認日: 2026-07-24 / GitHub Docs「Re-running workflows and jobs」: https://docs.github.com/en/actions/how-tos/manage-workflow-runs/re-run-workflows-and-jobs
- 確認日: 2026-07-24 / GitHub Docs「Enabling debug logging」: https://docs.github.com/en/actions/how-tos/monitor-workflows/enable-debug-logging
- 確認日: 2026-07-24 / GitHub Docs「Contexts」: https://docs.github.com/en/actions/concepts/workflows-and-actions/contexts
続き(結論と実データ)はnoteに置いています
ここでは手順のところまで書きました。実際に出た数字、うまくいかなかった条件、そのまま使える設定ファイルは、 note の記事にまとめてあります。