CI 里想让脚本遇到管道错误就失败,可以开 bash 的 pipefail

46 次浏览7 条回复

有些 CI 脚本用了管道,前面的命令失败时,最后一个命令仍可能返回 0,结果把失败步骤误判成成功。脚本开头可以写:

#!/usr/bin/env bash
set -Eeuo pipefail

make build | tee build.log

pipefail 会让管道返回最左侧的失败状态,-e 让未处理的失败退出,-u 则能尽早发现未定义变量。环境:Bash 4+。如果某一步允许失败,最好局部处理返回值,不要整段关闭检查;另外带有敏感参数的命令经过 tee 前也要确认日志内容可以落盘。

补个容易踩的点:set -e 在 if 条件、&&/|| 等上下文里有例外,所以它不等于完整的错误处理。若想分别拿到管道各段状态,管道结束后要立刻保存 PIPESTATUS,后续命令会覆盖它。

JonahLv1#1

补个容易踩的点:set -e 在 if 条件、&&/|| 等上下文里有例外,所以它不等于完整的错误处理。若想分别拿到管道各段状态,管道结束后要立刻保存 PIPESTATUS,后续命令会覆盖它。

还有个 CI 里挺实用的细节:如果日志只想保留在文件、不想让 tee 的输出污染终端,可以把它放到重定向里,但别把真正的构建命令包进一个会吞掉退出码的 || true。比如 make build 2>&1 | tee build.log 配合 pipefail,构建失败时整个步骤仍会失败;需要允许失败的步骤,最好单独标记并记录原因。

我会再留意一个 CI 细节:tee 默认会把输出同时打到终端和日志文件,日志很大时终端会比较吵。只想留文件的话可以写成 make build >build.log 2>&1;还想实时看日志,就保留 tee,再配合 pipefail。另外如果构建命令会输出密钥或令牌,先确认日志脱敏,别因为排查失败把敏感值写进 CI artifact。

JonahLv1#1

补个容易踩的点:set -e 在 if 条件、&&/|| 等上下文里有例外,所以它不等于完整的错误处理。若想分别拿到管道各段状态,管道结束后要立刻保存 PIPESTATUS,后续命令会覆盖它。

PIPESTATUS 这个提醒很关键,尤其是想在 CI 里输出更具体的失败信息时。比如:

set -o pipefail
make build 2>&1 | tee build.log
status=("${PIPESTATUS[@]}")
if (( status[0] != 0 )); then
  printf 'build failed: %s\n' "${status[0]}" >&2
  exit "${status[0]}"
fi

要先保存数组,再执行 printf 或其他命令,不然状态就被覆盖了。这里只关心构建命令的退出码;如果流水线更长,也可以把每一段状态分别记到 CI 日志里。

FinleyLv1#4

PIPESTATUS 这个提醒很关键,尤其是想在 CI 里输出更具体的失败信息时。比如:

set -o pipefail
make build 2>&1 | tee build.log
status=("${PIPESTATUS[@]}")
if (( status[0] != 0 )); then
  printf 'build failed: %s\n' "${status[0]}" >&2
  exit "${status[0]}"
fi

要先保存数组,再执行 printf 或其他命令,不然状态就被覆盖了。这里只关心构建命令的退出码;如果流水线更长,也可以把每一段状态分别记到 CI 日志里。

这里再补一个边界:只检查 status[0] 可能漏掉 tee 自己失败的情况,比如日志目录满了。构建成功但 tee 返回非 0 时,pipefail 仍会让整条管道失败;如果脚本要区分两类错误,可以同时检查 status[0] 和 status[1],并给日志写入失败单独报错。

Nathan03Lv1#5

这里再补一个边界:只检查 status[0] 可能漏掉 tee 自己失败的情况,比如日志目录满了。构建成功但 tee 返回非 0 时,pipefail 仍会让整条管道失败;如果脚本要区分两类错误,可以同时检查 status[0] 和 status[1],并给日志写入失败单独报错。

再补一句:pipefail 并不是返回“最左侧”的失败状态,而是返回管道中最右侧那个非 0 状态;如果所有命令都成功才是 0。要精确区分每一段,还是得像楼上那样立刻保存 PIPESTATUS。这点在多个命令串起来时挺容易影响 CI 的报错判断。

uitfLv1#6

再补一句:pipefail 并不是返回“最左侧”的失败状态,而是返回管道中最右侧那个非 0 状态;如果所有命令都成功才是 0。要精确区分每一段,还是得像楼上那样立刻保存 PIPESTATUS。这点在多个命令串起来时挺容易影响 CI 的报错判断。

这个纠正很关键。再提醒一个和 set -e 组合时的坑:失败的管道执行完后,脚本可能已经因为 errexit 退出,后面的 status=("${PIPESTATUS[@]}") 根本跑不到。需要检查状态时,可以把这段放进 if/|| 的受控上下文,或者临时关闭 errexit,先立刻保存 PIPESTATUS,再恢复设置。