Huny's Dev Blog
huny.dev

Debug Bash Pipeline Failures in CI with pipefail and PIPESTATUS

Use Bash pipefail and PIPESTATUS to find upstream failures hidden by tee or cat, and distinguish them from SIGPIPE when head closes a pipe early.

Hun Jang
Hun Jang Nov 9, 2026

If build | tee build.log passes in Linux CI but leaves an empty artifact, enable Bash’s set -o pipefail and inspect PIPESTATUS immediately after a failure. By default, the last command’s status determines the pipeline result. When a reader such as head finishes early, however, an upstream process can exit on SIGPIPE. This article reproduces both cases locally and shows a pattern for checking a producer’s complete output.

A two-stage pipe with an amber break on the left while the right segment remains active

Why the last command can hide an upstream failure

By default, Bash uses the last command’s exit status as the pipeline status. A build can emit partial output and exit with 7, while tee successfully reads and saves that output and exits with 0. CI can then record the entire pipeline as successful. With pipefail, Bash returns the status of the rightmost failing command in the pipeline, or 0 if all commands succeed. If multiple stages fail, the aggregate status alone cannot reveal every failure. GNU Bash pipelines

PIPESTATUS holds the exit statuses of the most recent foreground pipeline in left-to-right order. Running echo or assigning rc=$? as a separate command first replaces that array. Capture $? and PIPESTATUS in one array assignment as shown below. The first entry holds the aggregate status, and the rest hold the individual stage statuses. GNU Bash variables

Reproduce the exit statuses of each stage

Save the following as bash-pipeline-status.sh and run bash bash-pipeline-status.sh. The example disables errexit so each experiment can run after the previous failure. The mock build emits partial output and returns status 7.

Read the output and identify SIGPIPE

The script ran on Linux x86-64 with GNU Bash 5.2.37 and GNU Coreutils 9.7 on November 9, 2026. All six status assertions passed. The standalone 1 is the first line printed by the final experiment.

Both default and pipefail report stage statuses of (7 0). Only the aggregate status changes. In multiple, the rightmost of the two failing stages supplies status 9.

In early-close, head reads one line and exits. Once no process holds the read end open, writing to the pipe generates SIGPIPE. On this system, Bash reported termination by signal 13 as 128 + 13 = 141. The ignored-signal case ignores that signal under the same conditions. The write fails with EPIPE, and GNU yes returns 1. The example uses GNU env to set signal handling explicitly for both cases. Linux pipe(7) · GNU Bash exit status · GNU Coreutils env

Decide which failures CI should report

To keep a build log while catching build failures, select Bash explicitly, enable pipefail, and check the pipeline in an if. In the failure branch, capture the statuses before printing any additional diagnostics, then return an exit status that marks the CI step as failed. Exceptions to errexit can apply inside a function or compound command invoked in an if condition, so have functions explicitly propagate failures from critical internal commands. The example ran in Bash. CI-specific default shells and execution options were outside the test scope. GNU Bash set builtin

If the output is finite and you only need to display the first line, awk 'NR == 1 { print }' can consume the entire input without an explicit exit. It waits for the producer to finish, which makes it unsuitable for an infinite stream. When early exit is intentional, handle it after identifying the failing stage and the reason for its status. Globally ignoring 141 or adding || true to the entire pipeline can hide real build or log-write failures.

한국어 버전 · Bash pipefail로 CI 실패 찾기와 SIGPIPE 구분하기Bash pipefail로 CI 실패 찾기와 SIGPIPE 구분하기

추천 글