git push | tail devuelve código 0 aunque el push falle

Ejecutas el push recortando la salida para no llenar la pantalla, o lo lanzas desde una herramienta que solo informa del código de salida del comando completo:

git push origin main 2>&1 | tail -20

El resultado dice que el comando terminó con código 0 y das el trabajo por subido. Sin embargo, al revisar el servidor el remoto sigue con el commit anterior, y las últimas líneas que viste no explican el motivo:

$ git push origin main 2>&1 | tail -3
build: paso 39 de 40
build: paso 40 de 40
error: failed to push some refs to 'https://example.com/equipo/mi-app.git'
$ echo $?
0

Este artículo explica por qué una tubería (pipe) oculta el fallo de git push, cómo obtener el código de salida real y cómo comprobar que el remoto recibió tu commit.

Por qué pasa

El manual de Bash define el estado de salida de una tubería como el del último comando, salvo que la opción pipefail esté activa. En git push ... | tail el último comando es tail, que termina bien siempre que pueda imprimir algo. El código de git push se descarta sin avisar.

Además, la causa del fallo suele estar en otro lugar. Git ejecuta el hook pre-push antes de enviar nada y, según su documentación, si ese hook termina con un estado distinto de cero el push se aborta sin enviar nada. Cuando el hook ejecuta lint, comprobación de tipos o un build, puede fallar por motivos del entorno (un permiso denegado sobre un archivo bloqueado, falta de memoria). Su mensaje aparece al principio de la salida, justo la parte que tail -20 recorta.

En la prueba, con un hook que terminaba con error, git push devolvió 1, la tubería completa devolvió 0 y el remoto conservó el hash anterior. También se ve el segundo efecto: con tail -3 desapareció la línea con el motivo (un EPERM simulado) que el hook había impreso al inicio.

Solución

  1. No pases git push por una tubería. Redirige la salida completa a un archivo y lee el código justo después:

    git push origin main > push.log 2>&1
    echo "git push terminó con código $?"
    tail -n 20 push.log

    El echo va antes que tail porque $? guarda solo el estado del último comando ejecutado. El archivo conserva todo el mensaje, de modo que puedes buscar el motivo con grep -n -i error push.log.

  2. Si necesitas la tubería, activa pipefail. Con esta opción, el estado de la tubería pasa a ser el del último comando (el de más a la derecha) que terminó con un valor distinto de cero:

    set -o pipefail
    git push origin main 2>&1 | tail -n 20
    echo "código: $?"

    En la prueba imprimió código: 1. En un script, coloca set -o pipefail al principio; en una sesión interactiva afecta a todos los comandos siguientes.

  3. O consulta PIPESTATUS (variable de Bash). Es un arreglo con el estado de cada comando de la última tubería ejecutada en primer plano:

    git push origin main 2>&1 | tail -n 20
    echo "git push: ${PIPESTATUS[0]}  tail: ${PIPESTATUS[1]}"

    La salida de la prueba fue git push: 1 tail: 0. Debes leerla en el comando inmediatamente posterior a la tubería, porque cualquier otro comando la sobrescribe.

  4. Comprueba el remoto, no solo el código. git ls-remote muestra los hashes que tiene el servidor; el primero debe coincidir con el de tu HEAD:

    git ls-remote origin refs/heads/main
    git rev-parse HEAD

    Otra opción es actualizar la información remota y mirar el estado resumido:

    git fetch origin
    git status -sb

    Si la primera línea muestra ## main...origin/main [ahead 1], tienes un commit que el remoto no recibió.

  5. Corrige la causa real y repite el push sin tubería. Busca en push.log la primera línea de error y ejecuta a mano los comandos del hook (lint, tipos, build) para reproducirlo. Evita usar git push --no-verify solo para que el push pase: según la documentación de Git, esa opción omite el hook por completo, y los errores que detectaba llegarán al remoto.

Cómo verificar que quedó resuelto

git push origin main > push.log 2>&1; echo "código: $?"
git ls-remote origin refs/heads/main
git rev-parse HEAD
git status -sb

El código debe ser 0, el hash de ls-remote debe ser idéntico al de rev-parse y la primera línea de git status -sb no debe incluir [ahead ...]. En la prueba, antes de corregir la causa el hash remoto era distinto del local y el estado mostraba [ahead 1]; después de eliminar la causa del fallo del hook y empujar sin tubería, el código fue 0, ambos hashes coincidieron y el estado volvió a ## main...origin/main.

Variantes y errores relacionados

PowerShell: usa $LASTEXITCODE

git push origin main 2>&1 | Select-Object -Last 20
$LASTEXITCODE

En Windows PowerShell 5.1, tras esa tubería $LASTEXITCODE conservó el código de git (1), pero $? valía True porque el último comando de la tubería terminó bien. Para decidir si el push funcionó, usa $LASTEXITCODE y no $?.

Cualquier comando seguido de una tubería

El mismo problema afecta a npm run build | tail, docker build . | tail o pytest | tee resultado.log. Si el comando de la izquierda falla y el de la derecha no, el resultado visible es éxito. Aplica set -o pipefail o redirige a un archivo.

&& e if también se engañan

git push origin main 2>&1 | tail -n 20 && echo "subido"

Sin pipefail, este comando imprime subido aunque el push haya fallado; con pipefail activo, no lo imprime.

set -e no basta

En un script con set -e, un comando que falla en mitad de una tubería no detiene la ejecución, porque el estado que cuenta es el del último. En la prueba, false | cat no interrumpió el script con set -e y sí lo hizo con set -eo pipefail. Para scripts de despliegue o de CI, la combinación habitual es esta última.

"Everything up-to-date" también devuelve 0

Un push sin cambios nuevos termina con código 0 de forma legítima. Por eso conviene comparar siempre los hashes con git ls-remote y git rev-parse HEAD: te dicen si el remoto tiene tu commit, sin depender del texto ni del código.

Resumen

  • El código de salida de git push ... | tail es el de tail, no el de git push; un hook pre-push fallido queda oculto.
  • tail además recorta el motivo del fallo, que el hook suele imprimir al principio.
  • Evita la tubería: git push ... > push.log 2>&1; echo $?, o usa set -o pipefail / ${PIPESTATUS[0]} en Bash y $LASTEXITCODE en PowerShell.
  • Verifica siempre el resultado real con git ls-remote origin refs/heads/main frente a git rev-parse HEAD.

Referencias

Comentarios