Start Debugging

Fehlerbehebung: The command 'dotnet' could not be found auf CI

Ihr CI-Runner kann dotnet nicht auflösen, weil das SDK für diesen Schritt nicht installiert ist oder zwar installiert, aber nicht im PATH liegt. Verwenden Sie actions/setup-dotnet, fixieren Sie ein global.json und exportieren Sie DOTNET_ROOT und ~/.dotnet/tools.

Die Lösung: Ein CI-Schritt führt dotnet in einer Shell aus, in der das SDK entweder nicht installiert ist, nicht im PATH liegt oder auf eine Version fixiert ist, die Ihre global.json ausschließt. In GitHub Actions fügen Sie vor jedem dotnet-Aufruf einen Schritt actions/setup-dotnet@v4 ein, committen eine global.json, die zum angeforderten SDK passt, und exportieren in Linux-Containern DOTNET_ROOT sowie $HOME/.dotnet/tools. Der Fehler liegt fast nie im Runner-Image.

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

oder auf Windows-Runnern:

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

oder, auf Ubuntu nach dotnet-install.sh:

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

Diese Anleitung bezieht sich auf .NET 11 (SDK 11.0.100), actions/setup-dotnet@v4.0.1, die UseDotNet@2-Task von Azure DevOps in Version 2.213.x und das dotnet-install.sh, wie es unter https://dot.net/v1/dotnet-install.sh im Mai 2026 veröffentlicht ist. Die zugrundeliegenden Ursachen haben sich seit .NET Core 3.1 nicht geändert; nur die Action-Versionen.

Warum CI-Shells dotnet verlieren

Es gibt vier Ursachen. Sie sind leicht zu verwechseln, weil alle dieselbe command not found-Zeile zeigen, deshalb lohnt es sich, vor dem Patchen des YAML zu wissen, welche davon vorliegt.

  1. Das Runner-Image hat überhaupt kein SDK. Container-Images wie ubuntu:24.04, alpine:3.20 oder mcr.microsoft.com/devcontainers/base:ubuntu enthalten das .NET-SDK nicht. Von GitHub gehostete Runner (ubuntu-latest, windows-latest) enthalten es, aber die gecachte Version ist die, die das Runner-Image zufällig eingebacken hat, nicht die, die Ihr Repository benötigt.
  2. Das SDK ist installiert, aber für diesen Schritt nicht im PATH. Jeder Schritt in GitHub Actions läuft in einer frischen Shell. Eine Zeile in ~/.bashrc aus einem vorherigen Schritt überträgt sich nicht. Ein export von PATH in einem run:-Block leckt nicht in den nächsten run:-Block.
  3. Das SDK ist im PATH, aber global.json fixiert eine Version, die nicht installiert ist. Wenn dotnet startet, liest es die nächste global.json im Verzeichnisbaum nach oben und löst ein SDK auf, das zu version und rollForward passt. Findet sich keines, erhalten Sie error NETSDK1045 oder einen Host-Fehler, der je nach Host als „command not found”-förmige Meldung im Wrapper-Skript erscheint.
  4. Das SDK wurde von dotnet-install.sh in $HOME/.dotnet installiert, aber DOTNET_ROOT und PATH wurden nie gesetzt. Das ist der häufigste Fehler auf selbst gehosteten Linux-Runnern und in Docker-Containern. Das Skript installiert sauber, danach exportiert kein Folgeschritt die Variablen.

Eine minimale CI-Reproduktion

Speichern Sie dies als .github/workflows/build.yml und pushen Sie in ein Repository mit einer .csproj:

# .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

Der Schlüssel container: tauscht das Runner-Betriebssystem gegen ein nacktes Ubuntu-Image. Der Standard-Runner ubuntu-latest enthält das SDK, daher funktioniert das Snippet, wenn Sie container: entfernen. Die meisten Teams stolpern darüber, wenn sie einen Job aus Reproduzierbarkeitsgründen in einen Container verlagern und dabei vergessen, setup-dotnet mitzunehmen.

Lösung 1: SDK im selben Job installieren und dann nutzen

Die kanonische Lösung in GitHub Actions ist actions/setup-dotnet. Setzen Sie sie vor jeden Schritt, der dotnet aufruft. Sie lädt das SDK in einen Cache pro Runner, stellt es allen folgenden Schritten an den Anfang des PATH und exportiert DOTNET_ROOT für Tools, die das SDK-Installationsverzeichnis direkt benötigen.

# .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

Zwei Details, die wehtun:

In Azure DevOps ist das Pendant 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

In GitLab CI oder Buildkite ist der sauberste Weg ein Basis-Image mit eingebackenem SDK (mcr.microsoft.com/dotnet/sdk:11.0). Vermeiden Sie dotnet-install.sh im Job selbst, sofern nicht zwingend nötig: es funktioniert, aber jeder Job zahlt die Download-Kosten.

Lösung 2: global.json einchecken, die zum CI passt

Wenn CI dotnet build ausführt, verwendet es das SDK, das die global.json-Auflösung gewinnt, nicht das neueste installierte SDK. Ein typischer Fehler sieht so aus:

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]

Der Runner hat 11.0.100; global.json fordert 11.0.200. Das Wrapper-Skript beendet mit einem Exit-Code ungleich null, und je nach Host kann das als „command not found” erscheinen, weil ein Bash-if den eigentlichen Fehler verschluckt.

Halten Sie global.json ehrlich:

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

rollForward: latestFeature lässt einen Entwickler mit 11.0.103 arbeiten, ohne die Datei bei jedem Patch-Release anzupassen. latestMajor ist zu permissiv für CI, disable zu streng für lokal. Lassen Sie version mit dem übereinstimmen, was dotnet-version von actions/setup-dotnet installiert.

Lösung 3: wenn Sie dotnet-install.sh verwenden müssen

Innerhalb eines abgespeckten Containers oder auf einem selbst gehosteten Runner, auf dem Sie setup-dotnet nicht verwenden können, installieren Sie mit dem offiziellen Skript und exportieren die Variablen anschließend in jedem Folgeschritt explizit.

# 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

Die beiden echo-Zeilen schreiben in spezielle Dateien, die GitHub Actions zwischen Schritten liest: GITHUB_PATH stellt einen Eintrag an den Anfang des PATH für jeden folgenden Schritt im Job, und GITHUB_ENV exportiert eine Umgebungsvariable auf dieselbe Weise. export PATH=... innerhalb desselben run:-Blocks würde für den nächsten Schritt nicht greifen, und das ist die Falle, in die man tappt, wenn man ein Shell-Skript wörtlich übersetzt.

DOTNET_ROOT ist wichtig, auch wenn PATH gesetzt ist. Der Host (das Binary dotnet) nutzt DOTNET_ROOT, um die Ordner shared/Microsoft.NETCore.App und sdk/ zu finden. Korrigieren Sie nur PATH, kann es vorkommen, dass dotnet --info funktioniert, aber dotnet build mit einem Host-Fehler über eine fehlende Laufzeit scheitert. Laut Microsoft Learn wird DOTNET_ROOT vom Host auf Linux und macOS und auf Windows bei nicht standardmäßiger Installation gelesen.

Fügen Sie auch das tools-Verzeichnis hinzu. Ohne $HOME/.dotnet/tools im PATH gelingt zwar jeder dotnet tool install --global-Aufruf, das Tool ist aber unerreichbar und erzeugt den verwandten Fehler: dotnet-ef: command not found.

Lösung 4: vorgefertigtes SDK-Image, kein Installationsschritt

Für Docker-basiertes CI ist der reibungsärmste Weg, mit einem Image zu starten, das das SDK bereits enthält:

# .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

Spiegeln Sie das auf Buildkite, CircleCI, Jenkins-Agenten in Docker und jeder Plattform, deren CI-Primitiv „ein Container plus ein Skript” ist. Sie tauschen Flexibilität (ein Image, ein SDK) gegen die Garantie, dass dotnet ab dem ersten Befehl im PATH ist.

Häufige Varianten und Ähnliches

Suchanfragen, die auf dieser Seite landen, zielen manchmal auf einen leicht anderen Fehler. Es lohnt sich, früh abzugrenzen, um nicht die falsche Korrektur zu jagen.

Dinge, die wie Lösungen aussehen, aber keine sind

Die Lösung in CI verifizieren

Bevor Sie weitermachen, führen Sie zwei Diagnoseschritte aus. Sie sind günstig und ersparen die Jagd nach Phantomen in der dotnet build-Ausgabe.

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

which dotnet (oder where dotnet unter Windows) bestätigt, welches Binary die Shell auflöst. dotnet --info druckt die Laufzeit, die SDK-Liste und die aufgelöste global.json. Wenn --info erfolgreich ist, build aber mit „command not found” scheitert, liegt der Fehler in einem Wrapper-Skript, das Fehler verschluckt, nicht in dotnet selbst. Das ist der Moment, den Wrapper zu lesen, nicht neu zu installieren.

Wenn die --info-Ausgabe das angeforderte SDK zeigt, Base Path: auf das erwartete Verzeichnis zeigt und global.json file: <Ihr Pfad> listet, sind Sie fertig. Alles andere ist eine echte Fehlkonfiguration, die es zu beheben gilt.

Verwandt

Quellen

Comments

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

< Zurück