CI 里 `cmd | tee log` 变绿时,先确认有没有开 `pipefail`

67 次浏览6 条回复

有些 CI 脚本把测试输出接到 tee,最后拿到的却是 tee 的退出码,前面的测试挂了也可能显示成功:

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

pytest -q 2>&1 | tee test.log

环境:Bash 3+、pytest。pipefail 会让管道返回最右侧失败命令的状态;只想让这一步严格一点,也可以只写这一行,不必顺手改变整个脚本的 errexit 行为。CI 里再配合明确的退出码检查,日志和失败状态就不会互相打架。

补一个容易漏的点:CI 配置的 shell 可能不是 Bash,/bin/sh 下直接写 set -o pipefail 不一定可用。要区分测试命令和 tee 各自的退出码时,可以在 Bash 里看 PIPESTATUS;只关心整条流水线是否失败,pipefail 就够了。

FinleyLv1#1

补一个容易漏的点:CI 配置的 shell 可能不是 Bash,/bin/sh 下直接写 set -o pipefail 不一定可用。要区分测试命令和 tee 各自的退出码时,可以在 Bash 里看 PIPESTATUS;只关心整条流水线是否失败,pipefail 就够了。

对,PIPESTATUS 这点很实用,不过要紧跟在管道后面保存,别先执行 echo 之类的命令,不然数组可能已经变了。比如:

pytest -q 2>&1 | tee test.log
status=("${PIPESTATUS[@]}")
printf 'pytest=%s tee=%s\n' "${status[0]}" "${status[1]}"
(( status[0] == 0 && status[1] == 0 ))

如果 CI 明确用 /bin/sh,就别混入这套 Bash 写法,直接把 runner shell 配成 Bash 会更省心。

uitfLv1#2

对,PIPESTATUS 这点很实用,不过要紧跟在管道后面保存,别先执行 echo 之类的命令,不然数组可能已经变了。比如:

pytest -q 2>&1 | tee test.log
status=("${PIPESTATUS[@]}")
printf 'pytest=%s tee=%s\n' "${status[0]}" "${status[1]}"
(( status[0] == 0 && status[1] == 0 ))

如果 CI 明确用 /bin/sh,就别混入这套 Bash 写法,直接把 runner shell 配成 Bash 会更省心。

再补一个边界:如果测试外面套了 timeout 或自定义 wrapper,PIPESTATUS[0] 看到的是那层命令的退出码,不一定等于测试框架本身的结果。wrapper 最好明确透传退出码,并区分超时和测试失败;另外 tee 写盘失败也值得让 CI 失败,免得日志不完整却显示通过。

FinleyLv1#3

再补一个边界:如果测试外面套了 timeout 或自定义 wrapper,PIPESTATUS[0] 看到的是那层命令的退出码,不一定等于测试框架本身的结果。wrapper 最好明确透传退出码,并区分超时和测试失败;另外 tee 写盘失败也值得让 CI 失败,免得日志不完整却显示通过。

这个边界很关键。timeout 默认用 124 表示超时,但测试程序自己也可能返回 124,CI 里最好别把两者混成一种失败。可以在 wrapper 里先保存管道状态,再把“超时 / 测试失败 / tee 写盘失败”分别打到日志,最后按团队约定映射退出码。还有一点,set -e 在条件和管道上下文里有例外,别只靠它判断测试是否通过。

隔壁空白Lv1#4

这个边界很关键。timeout 默认用 124 表示超时,但测试程序自己也可能返回 124,CI 里最好别把两者混成一种失败。可以在 wrapper 里先保存管道状态,再把“超时 / 测试失败 / tee 写盘失败”分别打到日志,最后按团队约定映射退出码。还有一点,set -e 在条件和管道上下文里有例外,别只靠它判断测试是否通过。

这串讨论里最容易落坑的其实是 set -e:如果它包着管道,可能在你保存 PIPESTATUS 前就提前退出。建议把“执行管道、立刻复制状态、再做映射”放进同一个 Bash wrapper,先明确处理 tee 写盘失败,再决定测试失败和超时分别返回什么;不要只看最后一个退出码。另一个细节是,timeout 的超时码和测试程序自定义的同值返回码可能撞车,团队如果要区分,最好在 wrapper 里额外写明确的超时标记。

阿线Lv1#5

这串讨论里最容易落坑的其实是 set -e:如果它包着管道,可能在你保存 PIPESTATUS 前就提前退出。建议把“执行管道、立刻复制状态、再做映射”放进同一个 Bash wrapper,先明确处理 tee 写盘失败,再决定测试失败和超时分别返回什么;不要只看最后一个退出码。另一个细节是,timeout 的超时码和测试程序自定义的同值返回码可能撞车,团队如果要区分,最好在 wrapper 里额外写明确的超时标记。

这个场景里,wrapper 可以把 errexit 暂时关掉,明确保存状态后再恢复,避免还没复制 PIPESTATUS 就退出:

set +e
timeout 300s pytest -q 2>&1 | tee test.log
st=("${PIPESTATUS[@]}")
set -e

if (( st[1] != 0 )); then
  exit 2  # tee 写盘失败
fi
exit "${st[0]}"

如果还要区分超时和测试自身返回 124,单靠退出码还是不够,最好让 wrapper 写一个明确的超时标记。关键是 st 必须紧跟在管道后面赋值。