Start Debugging

解決: The command 'dotnet' could not be found が CI で出る

CI ランナーが dotnet を解決できないのは、そのステップで SDK が未インストール、もしくはインストール済みでも PATH に無いためです。actions/setup-dotnet を使い、global.json を固定し、DOTNET_ROOT と ~/.dotnet/tools をエクスポートしてください。

解決策: CI のあるステップが、SDK が未インストール、PATH に無い、または global.json が禁じるバージョンに固定された状態で dotnet を実行しています。GitHub Actions では、dotnet を呼び出す前に actions/setup-dotnet@v4 のステップを置き、要求した SDK と一致する global.json をコミットし、Linux コンテナーでは DOTNET_ROOT$HOME/.dotnet/tools をエクスポートしてください。このエラーがランナーイメージのバグであることはほぼありません。

/bin/bash: line 1: dotnet: command not found
##[error]Process completed with exit code 127.

または Windows ランナーで:

dotnet : The term 'dotnet' is not recognized as the name of a cmdlet, function, script file, or operable program.
At line:1 char:1
+ dotnet build
+ ~~~~~~
    + CategoryInfo          : ObjectNotFound: (dotnet:String) [], CommandNotFoundException

または Ubuntu で dotnet-install.sh の後に:

Command 'dotnet' not found, but can be installed with:
sudo apt install dotnet-host

このガイドは .NET 11 (SDK 11.0.100)、actions/setup-dotnet@v4.0.1、Azure DevOps の UseDotNet@2 タスク 2.213.x、および 2026 年 5 月時点で https://dot.net/v1/dotnet-install.sh に公開されている dotnet-install.sh を対象にしています。根本原因は .NET Core 3.1 以降変わっていません。変わったのは action のバージョンだけです。

なぜ CI のシェルが dotnet を見失うのか

根本原因は 4 つあります。いずれも同じ command not found の行を出すので混同しやすく、YAML を直す前にどれに当たっているかを知っておく価値があります。

  1. ランナーイメージにそもそも SDK が無い。 ubuntu:24.04alpine:3.20mcr.microsoft.com/devcontainers/base:ubuntu などのコンテナーイメージには .NET SDK は含まれません。GitHub ホスト型ランナー (ubuntu-latestwindows-latest) には含まれていますが、キャッシュされたバージョンはたまたまイメージに焼かれたものであり、リポジトリーが必要とするものとは限りません。
  2. SDK はインストール済みだが、このステップの PATH には無い。 GitHub Actions の各ステップは新しいシェルで実行されます。前のステップで ~/.bashrc に行を追加しても引き継がれません。run: ブロック内で export した PATH は次の run: ブロックに漏れません。
  3. SDK は PATH にあるが、global.json が未インストールのバージョンに固定している。 dotnet 起動時、最も近い global.json をディレクトリツリー上方向に読み、versionrollForward のルールに合う SDK を解決します。一致するものが無ければ error NETSDK1045 か、ホストの失敗が出て、ホストによってはラッパースクリプト上で “command not found” の形に化けて現れます。
  4. SDK は dotnet-install.sh$HOME/.dotnet にインストールされたが、DOTNET_ROOTPATH が設定されていない。 これがセルフホスト Linux ランナーや Docker コンテナー内で最もよく見る失敗です。スクリプトは綺麗にインストールしますが、その後のステップで変数をエクスポートしません。

最小限の CI 再現

これを .github/workflows/build.yml として保存し、.csproj のあるリポジトリーに push してください:

# .github/workflows/build.yml -- .NET 11, GitHub Actions May 2026
name: build
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    container: ubuntu:24.04   # no SDK is preinstalled here
    steps:
      - uses: actions/checkout@v4
      - run: dotnet --info     # fails: dotnet: command not found

container: キーはランナーの OS を素の Ubuntu イメージに差し替えます。既定の ubuntu-latest ランナーには SDK が入っているので、container: を外すとこのスニペットは動きます。多くのチームは、再現性のためにジョブをコンテナーへ移し、setup-dotnet を一緒に持っていくのを忘れた時にこれにぶつかります。

解決策 1: 同じジョブで SDK をインストールし、それを使う

GitHub Actions の正攻法は actions/setup-dotnet です。dotnet を呼ぶ任意のステップの前に置きます。SDK をランナー単位のキャッシュにダウンロードし、後続のすべてのステップで PATH の先頭に追加し、SDK のインストールディレクトリーを直接必要とするツールのために DOTNET_ROOT をエクスポートします。

# .github/workflows/build.yml -- setup-dotnet@v4
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: "11.0.x"
      - run: dotnet --info
      - run: dotnet build -c Release

刺さる細部が 2 つ:

Azure DevOps では同等の UseDotNet@2 を使います:

# azure-pipelines.yml -- Azure DevOps, UseDotNet@2
steps:
  - task: UseDotNet@2
    inputs:
      packageType: sdk
      version: "11.0.x"
  - script: dotnet build -c Release

GitLab CI や Buildkite では、SDK をあらかじめ焼き込んだベースイメージ (mcr.microsoft.com/dotnet/sdk:11.0) が最も綺麗な経路です。やむを得ない場合を除き、ジョブ内での dotnet-install.sh 実行は避けてください。動きはしますが、ジョブごとにダウンロードコストを払います。

解決策 2: CI と一致する global.json をコミットする

CI が dotnet build を実行すると、最新のインストール済み SDK ではなく、global.json の解決で勝った SDK を使います。よくある失敗はこんな形です:

A compatible .NET SDK was not found.
Requested SDK version: 11.0.200
global.json file: /home/runner/work/myrepo/myrepo/global.json
Installed SDKs:
  8.0.412 [/usr/share/dotnet/sdk]
  11.0.100 [/usr/share/dotnet/sdk]

ランナーには 11.0.100 が、global.json は 11.0.200 を要求しています。ラッパースクリプトは非ゼロで終了し、ホストによっては、本当のエラーを飲み込んだ Bash の if から “command not found” が伝播して見えます。

global.json は正直に保ちましょう:

{
  "sdk": {
    "version": "11.0.100",
    "rollForward": "latestFeature"
  }
}

rollForward: latestFeature なら、11.0.103 を入れた開発者がパッチリリースごとにファイルを上げなくても作業できます。latestMajor は CI には緩すぎ、disable はローカルには厳しすぎます。versionactions/setup-dotnetdotnet-version がインストールするものに合わせてください。

解決策 3: どうしても dotnet-install.sh が必要なとき

切り詰めたコンテナー内、あるいは setup-dotnet を使えないセルフホストランナーでは、公式スクリプトでインストールし、その後のすべてのステップで明示的に変数をエクスポートしてください。

# self-hosted runner or restrictive container -- .NET 11
jobs:
  build:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - name: Install .NET 11 SDK
        run: |
          curl -sSL https://dot.net/v1/dotnet-install.sh -o dotnet-install.sh
          chmod +x dotnet-install.sh
          ./dotnet-install.sh --channel 11.0 --install-dir "$HOME/.dotnet"
          echo "$HOME/.dotnet" >> "$GITHUB_PATH"
          echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH"
          echo "DOTNET_ROOT=$HOME/.dotnet" >> "$GITHUB_ENV"
      - run: dotnet --info
      - run: dotnet tool restore && dotnet build -c Release

2 行の echo は、GitHub Actions がステップ間で読む特別なファイルに書き込みます。GITHUB_PATH はジョブ内の以後すべてのステップで PATH の先頭にエントリーを追加し、GITHUB_ENV は同じ仕組みで環境変数をエクスポートします。同じ run: ブロック内の export PATH=... は次のステップでは効かないため、シェルスクリプトを字面どおりに移植すると人はここにはまります。

DOTNET_ROOTPATH が設定済みでも重要です。ホスト (dotnet バイナリー) は DOTNET_ROOT を使って shared/Microsoft.NETCore.Appsdk/ フォルダーを見つけます。PATH だけ直すと、dotnet --info は動くが dotnet build がランタイム不足のホストエラーで落ちる、という状態になり得ます。Microsoft Learn によれば、DOTNET_ROOT は Linux と macOS のホストで読まれ、Windows では既定外のインストール時に読まれます。

tools ディレクトリーも追加してください。$HOME/.dotnet/toolsPATH に無いと、dotnet tool install --global の呼び出しは成功してもツールには手が届かず、関連エラー dotnet-ef: command not found が出ます。

解決策 4: ビルド済み SDK イメージ、インストールステップなし

Docker ベースの CI では、最も摩擦の少ない経路は SDK が既に入ったイメージから始めることです:

# .gitlab-ci.yml -- pinned SDK image, no install step
build:
  image: mcr.microsoft.com/dotnet/sdk:11.0
  script:
    - dotnet --info
    - dotnet build -c Release

これを Buildkite、CircleCI、Docker 上の Jenkins エージェント、そして CI のプリミティブが「コンテナー+スクリプト」のあらゆるプラットフォームへ展開してください。柔軟性 (1 つのイメージ、1 つの SDK) を犠牲にして、最初のコマンドから dotnetPATH にあることを保証します。

よくある変種と紛らわしい亜種

このページに辿り着く検索が、わずかに違うエラーを探している場合があります。間違った修正を追わないよう、先に切り分けておきましょう。

解決策に見えて違うもの

CI で解決を検証する

次に進む前に、診断ステップを 2 つ走らせます。安価で、dotnet build の出力で幻影を追うことを防げます。

- run: which dotnet || command -v dotnet || true
- run: dotnet --info

which dotnet (Windows では where dotnet) は、シェルがどのバイナリーを解決するかを確定させます。dotnet --info はランタイム、SDK 一覧、解決された global.json を出力します。--info が成功するのに build が “command not found” で落ちるなら、失敗は dotnet 自身ではなく、エラーを飲み込むラッパースクリプトの中にあります。そのときは再インストールではなく、ラッパーを読む時間です。

--info の出力が要求した SDK を示し、Base Path: が期待したディレクトリーを指し、global.json file: <あなたのパス> を列挙したら完了です。それ以外は本当の設定誤りで、直す価値があります。

関連

出典

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る