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
-
No pases
git pushpor 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.logEl
echova antes quetailporque$?guarda solo el estado del último comando ejecutado. El archivo conserva todo el mensaje, de modo que puedes buscar el motivo congrep -n -i error push.log. -
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, colocaset -o pipefailal principio; en una sesión interactiva afecta a todos los comandos siguientes. -
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. -
Comprueba el remoto, no solo el código.
git ls-remotemuestra los hashes que tiene el servidor; el primero debe coincidir con el de tuHEAD:git ls-remote origin refs/heads/main git rev-parse HEADOtra opción es actualizar la información remota y mirar el estado resumido:
git fetch origin git status -sbSi la primera línea muestra
## main...origin/main [ahead 1], tienes un commit que el remoto no recibió. -
Corrige la causa real y repite el push sin tubería. Busca en
push.logla primera línea de error y ejecuta a mano los comandos del hook (lint, tipos, build) para reproducirlo. Evita usargit push --no-verifysolo 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 ... | tailes el detail, no el degit push; un hookpre-pushfallido queda oculto. tailademá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 usaset -o pipefail/${PIPESTATUS[0]}en Bash y$LASTEXITCODEen PowerShell. - Verifica siempre el resultado real con
git ls-remote origin refs/heads/mainfrente agit rev-parse HEAD.
Comentarios
Publicar un comentario