Start Debugging

Solución: The command 'dotnet' could not be found en CI

Tu runner de CI no puede resolver dotnet porque el SDK no está instalado para ese paso, o sí lo está pero no en PATH. Usa actions/setup-dotnet, fija un global.json y exporta DOTNET_ROOT y ~/.dotnet/tools.

La solución: un paso de CI ejecuta dotnet en un shell donde el SDK no está instalado, no está en PATH, o está fijado a una versión que tu global.json no permite. En GitHub Actions, añade un paso actions/setup-dotnet@v4 antes de cualquier invocación de dotnet, incluye en el repo un global.json que coincida con el SDK que pides, y en contenedores Linux exporta DOTNET_ROOT y $HOME/.dotnet/tools. El error casi nunca es un fallo en la imagen del runner.

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

o en runners de 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

o, en Ubuntu después de dotnet-install.sh:

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

Esta guía está escrita contra .NET 11 (SDK 11.0.100), actions/setup-dotnet@v4.0.1, la tarea UseDotNet@2 de Azure DevOps versión 2.213.x, y dotnet-install.sh tal como se publica en https://dot.net/v1/dotnet-install.sh en mayo de 2026. Las causas subyacentes no han cambiado desde .NET Core 3.1; solo han cambiado las versiones de las acciones.

Por qué los shells de CI pierden dotnet

Hay cuatro causas raíz. Es fácil confundirlas porque todas muestran la misma línea command not found, así que conviene saber cuál estás viendo antes de parchear el YAML.

  1. La imagen del runner no tiene SDK alguno. Imágenes de contenedor como ubuntu:24.04, alpine:3.20 o mcr.microsoft.com/devcontainers/base:ubuntu no incluyen el SDK de .NET. Los runners alojados por GitHub (ubuntu-latest, windows-latest) sí, pero la versión en caché es la que se haya horneado en la imagen del runner, no la que necesita tu repo.
  2. El SDK está instalado, pero no está en PATH para este paso. Cada paso en GitHub Actions corre en un shell nuevo. Añadir una línea a ~/.bashrc desde un paso anterior no se traslada. Hacer export de PATH dentro de un bloque run: no se filtra al siguiente bloque run:.
  3. El SDK está en PATH, pero global.json fija una versión que no está instalada. Cuando dotnet arranca, lee el global.json más cercano subiendo el árbol de directorios y resuelve un SDK que cumple las reglas de version y rollForward. Si no hay coincidencia, obtienes error NETSDK1045 o un fallo del host que aparece, según el caso, con forma de “command not found” en el script envoltorio.
  4. El SDK fue instalado por dotnet-install.sh en $HOME/.dotnet, pero nunca se establecieron DOTNET_ROOT ni PATH. Este es el fallo más común en runners auto-alojados de Linux y dentro de contenedores Docker. El script instala correctamente, luego ningún paso posterior exporta las variables.

Una reproducción mínima en CI

Guarda esto como .github/workflows/build.yml y empújalo a un repo con un .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

La clave container: cambia el SO del runner por una imagen Ubuntu pelada. El runner por defecto ubuntu-latest sí trae el SDK, así que quitar container: hace que este snippet funcione. La mayoría de equipos chocan con esto al mover un job a un contenedor por reproducibilidad y olvidar llevar también setup-dotnet.

Solución 1: instalar el SDK en el mismo job y luego usarlo

La solución canónica en GitHub Actions es actions/setup-dotnet. Colócalo antes de cualquier paso que llame a dotnet. Descarga el SDK a una caché por runner, lo antepone al PATH para todos los pasos siguientes y exporta DOTNET_ROOT para herramientas que necesitan el directorio de instalación del SDK directamente.

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

Dos detalles que muerden:

En Azure DevOps, el equivalente es 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

En GitLab CI o Buildkite, el camino más limpio es una imagen base con el SDK ya horneado (mcr.microsoft.com/dotnet/sdk:11.0). Evita ejecutar dotnet-install.sh en el propio job a no ser que sea estrictamente necesario: funciona, pero cada job paga el coste de descarga.

Solución 2: incluye un global.json que coincida con CI

Cuando CI ejecuta dotnet build, usa el SDK que gana la resolución de global.json, no el último SDK instalado. Un fallo típico tiene esta pinta:

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]

El runner tiene 11.0.100; global.json pide 11.0.200. El script envoltorio sale con código distinto de cero, y según el host, puedes ver “command not found” propagado desde un if de Bash que se tragó el error real.

Mantén global.json honesto:

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

rollForward: latestFeature deja a un desarrollador con 11.0.103 trabajar sin tener que subir el archivo en cada release de parche. latestMajor es demasiado permisivo para CI; disable es demasiado estricto para local. Haz que version coincida con lo que dotnet-version de actions/setup-dotnet va a instalar.

Solución 3: cuando tienes que usar dotnet-install.sh

Dentro de un contenedor reducido, o en un runner auto-alojado donde no puedes usar setup-dotnet, instala con el script oficial y exporta las variables explícitamente en cada paso posterior.

# 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

Las dos líneas echo escriben en archivos especiales que GitHub Actions lee entre pasos: GITHUB_PATH antepone una entrada al PATH para todos los pasos siguientes del job, y GITHUB_ENV exporta una variable de entorno de la misma manera. export PATH=... dentro del mismo bloque run: no funcionaría para el paso siguiente, que es la trampa en la que cae la gente cuando traduce un script de shell literalmente.

DOTNET_ROOT importa aunque PATH esté configurado. El host (el binario dotnet) usa DOTNET_ROOT para encontrar las carpetas shared/Microsoft.NETCore.App y sdk/. Si solo arreglas PATH, puedes acabar con dotnet --info funcionando pero dotnet build fallando con un error del host sobre un runtime que falta. Según Microsoft Learn, DOTNET_ROOT lo lee el host en Linux y macOS, y en Windows cuando la instalación no está en la ubicación por defecto.

Añade también el directorio tools. Sin $HOME/.dotnet/tools en PATH, cualquier llamada dotnet tool install --global tiene éxito pero la herramienta queda inaccesible, produciendo el error relacionado: dotnet-ef: command not found.

Solución 4: imagen del SDK preconstruida, sin paso de instalación

Para CI basado en Docker, el camino con menor fricción es partir de una imagen que ya tiene el 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

Replica esto en Buildkite, CircleCI, agentes de Jenkins en Docker y cualquier plataforma cuya primitiva de CI sea “un contenedor más un script”. Sacrificas flexibilidad (una imagen, un SDK) a cambio de la garantía de que dotnet está en PATH desde el primer comando.

Variantes comunes y errores parecidos

Las búsquedas que aterrizan en esta página a veces buscan un error ligeramente distinto. Conviene diferenciarlas de entrada para no perseguir la solución equivocada.

Cosas que parecen soluciones pero no lo son

Verificar la solución en CI

Antes de seguir, ejecuta dos pasos de diagnóstico. Son baratos y te ahorran perseguir fantasmas en la salida de dotnet build.

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

which dotnet (o where dotnet en Windows) confirma qué binario resuelve el shell. dotnet --info imprime el runtime, la lista de SDKs y el global.json resuelto. Si --info tiene éxito pero build falla con “command not found”, el fallo está dentro de un script envoltorio que se traga errores, no en dotnet. Ese es el momento de leer el envoltorio, no de reinstalar.

Cuando la salida de --info muestre el SDK que pediste, apunte Base Path: al directorio que esperabas y liste global.json file: <tu ruta>, has terminado. Cualquier otra cosa es una configuración incorrecta real que vale la pena arreglar.

Relacionados

Fuentes

Comments

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

< Volver