Error Analysis in Deploy Scripts: Reproducible and Logged
AI generated
Bash · Deploy Scripts · Error Analysis · Debugging · Post-Mortem
Error Analysis in Deploy Scripts
reproducible and logged with set -x, trap ERR and LINENO

A deploy error that cannot be reproduced is not a solved error, it is deferred chaos. set -x, trap ERR, LINENO and structured log files make every error in deploy scripts traceable, reproducible and usable as the foundation for post-mortem analyses.

14 min read set -x · trap ERR · LINENO · BASH_LINENO · log files · post-mortem Bash 4.x · 5.x · Linux · CI/CD

1. The problem with deploy errors that leave no trace

A failed deploy without a meaningful log is a nightmare for any team. The questions are always the same: which step failed? What value did a given variable hold? Was the server even reachable? Was the error deterministic, or was a race condition involved? Without systematic error analysis in deploy scripts, root-causing stays guesswork, and the fix is at best a patch for the symptom, not the cause.

The real problem is not a lack of information as such, but a lack of reproducible information. Bash scripts produce little output by default. No timestamp, no line numbers, no expanded variable values. What the developer sees is a cryptic fragment of an error message, if anything at all. The tools for systematic error analysis in deploy scripts already exist in Bash: set -x, trap ERR, LINENO, BASH_LINENO and structured log files. They just need to be used consistently.

The difference between a team that handles deploys analytically and one that guesses anew at every outage lies exactly here: in the quality of the logging and the rigor of the error analysis in deploy scripts. A deploy script that, on failure, states precisely which command failed on which line with which exit code, which environment variables were active, and what output the failed command produced, is the foundation for an analysis measured in minutes instead of hours.

2. set -x: seeing every command with expanded values

set -x is the most powerful debugging tool for error analysis in deploy scripts. It enables trace mode: every command that runs is printed to stderr before execution, with variables fully expanded, globs resolved and subshell results included. The prefix + (or ++ for commands inside subshells) marks each trace line. This makes it immediately visible which variables held which concrete values, and whether a path or argument expanded as expected.

The pattern for selective tracing: check ${DEBUG:-0} at the start of the script and only enable set -x when DEBUG=1. That way the script runs in production without trace output, but can be switched into debug mode at any time by setting the environment variable, with no code changes required. For even more precise tracing, set -x and set +x can be placed around specific sections inside the script, so only the suspect code block gets traced. BASH_XTRACEFD redirects trace output to its own file so it does not get mixed in with regular stdout/stderr.


#!/usr/bin/env bash
# deploy-with-tracing.sh: Deploy script with full debug capabilities
set -euo pipefail
IFS=$'\n\t'

readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly LOG_DIR="/var/log/deploy"
readonly TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
readonly LOG_FILE="${LOG_DIR}/${SCRIPT_NAME%.sh}-${TIMESTAMP}.log"
readonly TRACE_FILE="${LOG_DIR}/${SCRIPT_NAME%.sh}-${TIMESTAMP}.trace"

mkdir -p "$LOG_DIR"

# Redirect stdout and stderr to log file while also showing on terminal
exec > >(tee -a "$LOG_FILE") 2>&1

# Enable tracing to separate file if DEBUG=1
if [[ "${DEBUG:-0}" == "1" ]]; then
  exec {BASH_XTRACEFD}>>"$TRACE_FILE"
  set -x
  echo "[DEBUG] Trace output: $TRACE_FILE"
fi

log() {
  local level="$1"; shift
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] [$SCRIPT_NAME:${BASH_LINENO[0]}] $*"
}

log INFO "Deploy started: PID=$$, USER=$USER, PWD=$PWD"
log INFO "Environment: DEPLOY_ENV=${DEPLOY_ENV:-unset}"

# Trace selectively around the critical section only
set +x  # Ensure tracing is off here
log INFO "Starting database migration..."
# Temporarily enable tracing for this specific step
{ set -x; bin/magento setup:upgrade; set +x; } 2>>"$TRACE_FILE"
log INFO "Database migration completed"

3. trap ERR: catching and logging errors immediately

trap ERR registers a function that runs on every command that returns a non-zero exit code, before the script is terminated by set -e. This is the ideal point for error analysis in deploy scripts: the ERR handler can print the exit code, line number, function name and stack trace before the process ends. That way the log file always contains a precise error message that points directly to the failing spot.

The ERR handler gets the line number via ${BASH_LINENO[0]}, the current function call stack via ${FUNCNAME[*]}, and the current file via ${BASH_SOURCE[0]}. Together these three form a complete stack trace, similar to what higher-level programming languages provide. Combined with printing the most recent log lines, this enables error analysis in deploy scripts without interactive debugging: the log file alone is enough to locate and understand the error.


#!/usr/bin/env bash
# error-handler.sh: Comprehensive error handling for deploy scripts
set -euo pipefail

readonly LOG_FILE="/var/log/deploy/current.log"
readonly NOTIFY_EMAIL="${NOTIFY_EMAIL:-}"

# Full stack trace on any error
err_handler() {
  local exit_code=$?
  local line_number="${BASH_LINENO[0]}"
  local command="${BASH_COMMAND}"

  echo "" >&2
  echo "════════════════════════════════════════════════════════════" >&2
  echo "[ERROR] Command failed with exit code $exit_code" >&2
  echo "[ERROR] Failed command: $command" >&2
  echo "[ERROR] Line: $line_number in ${BASH_SOURCE[1]:-$0}" >&2
  echo "" >&2
  echo "Call stack (most recent first):" >&2
  local i
  for (( i=1; i<${#FUNCNAME[@]}; i++ )); do
    local func="${FUNCNAME[$i]}"
    local src="${BASH_SOURCE[$i]:-main}"
    local lineno="${BASH_LINENO[$((i-1))]}"
    echo "  #$((i-1))  $func() at $src:$lineno" >&2
  done
  echo "" >&2

  # Capture last 20 lines of log for context
  echo "Last log entries:" >&2
  tail -20 "$LOG_FILE" 2>/dev/null | sed 's/^/  /' >&2
  echo "════════════════════════════════════════════════════════════" >&2

  # Send alert if email is configured
  if [[ -n "$NOTIFY_EMAIL" ]] && command -v mail &>/dev/null; then
    {
      echo "Deploy failed at line $line_number"
      echo "Command: $command"
      echo "Exit code: $exit_code"
      echo ""
      echo "Last 50 log lines:"
      tail -50 "$LOG_FILE" 2>/dev/null
    } | mail -s "[DEPLOY FAIL] $(hostname) - exit $exit_code" "$NOTIFY_EMAIL"
  fi
}

trap err_handler ERR

# Cleanup on any exit
cleanup() {
  local exit_code=$?
  if [[ $exit_code -eq 0 ]]; then
    echo "[OK] Deploy completed successfully"
  else
    echo "[FAIL] Deploy aborted with exit code $exit_code"
  fi
}
trap cleanup EXIT

4. LINENO and BASH_LINENO: locating the error in the script

LINENO is a variable automatically set by Bash that holds the current line number in the script. It gets updated with every command and is indispensable in logging functions for precise error analysis in deploy scripts. The pattern: log() { echo "[${BASH_LINENO[0]}] $*"; }, the caller implicitly passes on LINENO via the BASH_LINENO array, which holds every line number in the call stack.

BASH_LINENO is an array where ${BASH_LINENO[0]} holds the line number of the caller of the current function. Inside an ERR handler, ${BASH_LINENO[0]} points to the line that caused the error. ${BASH_LINENO[1]} points to the line from which the failing function was called. This array enables a complete call-stack trace, which is essential for reliable error analysis in deploy scripts: you see not only where, but also how the script arrived at the failing spot.

5. Structured log files for deploy scripts

Structured log files are the foundation of any serious error analysis in deploy scripts. A structured log file holds, for every line: timestamp, log level, script name, line number and the actual message. These five fields make it possible to compare logs across multiple deployments, correlate points in time and detect error patterns. An unstructured log file, a pile of echo calls with no consistent format, is worthless for automated analysis.

The most efficient pattern: exec > >(tee -a "$LOG_FILE") 2>&1 at the start of the script redirects all output (stdout and stderr) to both the terminal and the log file simultaneously. A logging function wraps all echo calls and adds metadata. The log file gets a timestamp in its name, so every deploy run has its own log file and logs are never overwritten. Log rotation via a separate cron job or a logrotate configuration keeps the log directory from growing unchecked.


#!/usr/bin/env bash
# logging-library.sh: Structured logging for deploy scripts
# Source this file: source /usr/local/lib/deploy/logging.sh

readonly LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3 [FATAL]=4)
LOG_LEVEL="${LOG_LEVEL:-INFO}"
LOG_FILE="${LOG_FILE:-/var/log/deploy/deploy.log}"
LOG_FORMAT="${LOG_FORMAT:-text}"  # text or json

_log_write() {
  local level="$1"; shift
  local message="$*"
  local timestamp
  timestamp="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
  local caller_line="${BASH_LINENO[1]}"
  local caller_func="${FUNCNAME[2]:-main}"
  local caller_file="${BASH_SOURCE[2]:-unknown}"

  # Filter by log level
  if [[ "${LOG_LEVELS[$level]:-0}" -lt "${LOG_LEVELS[$LOG_LEVEL]:-1}" ]]; then
    return 0
  fi

  if [[ "$LOG_FORMAT" == "json" ]]; then
    printf '{"timestamp":"%s","level":"%s","file":"%s","line":%d,"func":"%s","msg":%s}\n' \
      "$timestamp" "$level" "$(basename "$caller_file")" \
      "$caller_line" "$caller_func" \
      "$(printf '%s' "$message" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')"
  else
    printf '[%s] [%-5s] [%s:%d] %s\n' \
      "$timestamp" "$level" "$(basename "$caller_file")" "$caller_line" "$message"
  fi | tee -a "$LOG_FILE"
}

log_debug() { _log_write DEBUG "$@"; }
log_info()  { _log_write INFO  "$@"; }
log_warn()  { _log_write WARN  "$@" >&2; }
log_error() { _log_write ERROR "$@" >&2; }
log_fatal() { _log_write FATAL "$@" >&2; exit 1; }

# Usage example:
# LOG_FORMAT=json LOG_LEVEL=DEBUG source logging.sh
# log_info "Starting deployment of version $VERSION"
# log_error "Failed to connect to database: $db_error"

6. Collecting context information when an error occurs

A stack trace and an error message are necessary for error analysis in deploy scripts, but not sufficient. To truly understand an error you need context: which environment variables were set? Which services were reachable? What was the memory and CPU state at the time of the error? Which other processes were running in parallel? This information must be collected automatically when an error occurs so it is available for post-mortem analysis.

An error analysis in deploy scripts context collector is a function inside the ERR handler that runs automatically on failure: env | sort for all environment variables, df -h for disk usage, free -h for memory, netstat -tn or ss -tn for open connections, ps aux for running processes. Writing all of this into a context file that is kept alongside the log file makes later analyses far more efficient.

7. Making errors reproducible: environment snapshots

Reproducibility is the core goal of any error analysis in deploy scripts. An error that cannot be reproduced cannot be reliably fixed. The most common reason for non-reproducibility: the exact environment at the time of the error is unknown. Which dependency version was installed? Which configuration was active? Was a particular service down that is running again by the time you retest?

Environment snapshots solve this problem for error analysis in deploy scripts. At the start of the deployment, a snapshot of the relevant environment information is saved: installed package versions (dpkg --get-selections or rpm -qa), active service status (systemctl list-units --state=running), configuration file checksums, the network routing table, and active connections. When a deploy error occurs, the snapshot taken at the time of the error can be compared with the snapshot of a successful deployment. Differences between the snapshots are potential root causes.

8. Post-mortem process for deploy errors

A post-mortem is not a blame record, it is a structured analysis process that ensures a deploy error does not happen twice. The foundation for every post-mortem on error analysis in deploy scripts: the complete log file of the failed deployment, the ERR handler stack trace, the environment snapshot, and the timeline of events. Without these artifacts, a post-mortem is speculation.

An effective post-mortem process for error analysis in deploy scripts follows five steps. First: isolate the exact moment of failure from the log file, timestamp, failed command, exit code, line number. Second: evaluate the context snapshot, what was different from the last successful deploy? Third: reproduce the error on a test server with an identical environment. Fourth: implement the fix and verify it in a staging deploy. Fifth: extend the deploy script with an automatic check that catches exactly the condition that caused the failure, so the fix gets built structurally into the script instead of just being documented.

9. Debugging approaches compared

Several approaches are available for error analysis in deploy scripts, and they differ in information density, intrusiveness and suitability for different scenarios.

Approach Information density Intrusiveness Best use case
set -x Very high, every command is expanded High, large amount of output Local debugging, BASH_XTRACEFD into a file
trap ERR High, stack trace on error Low, only on failure Production, always active, no overhead
Structured logging Medium, defined measurement points Low, only explicit Production, timeline reconstruction
Environment snapshot High, full system state Medium, one-time at start Reproducibility, post-mortem comparison
ShellCheck Medium, static analysis None, runs before deployment CI pipeline, catching errors before deploy

The table makes the point clear: complete error analysis in deploy scripts requires all four approaches combined. ShellCheck and static analysis catch many errors before deployment. Structured logging and trap ERR stay active at all times. set -x and environment snapshots get switched on as needed, or generated as a standard artifact of every deployment. The combination ensures that no deploy error ever goes without a complete analysis.

Mironsoft

Deploy automation, error analysis and post-mortem tooling

Want to analyze deploy errors reproducibly instead of guessing?

We retrofit existing deploy scripts with trap ERR, structured logging, environment snapshots and post-mortem tooling, so every deploy error is fully documented and reproducible.

ERR handler

Stack trace, exit code, line number and context collector on every error

Logging infrastructure

Structured JSON or text logs with timestamp, level and line numbers

Post-mortem tooling

Environment snapshots, diff tools and a structured post-mortem process

10. Summary

Reproducible error analysis in deploy scripts is not a luxury, it is the foundation of reliable automation. set -x makes every executed command visible with fully expanded values. trap ERR catches every error and logs the stack trace, line number and context. LINENO and BASH_LINENO show precisely where in the script an error occurred. Structured log files make deploys comparable over time and automatically analyzable.

The decisive value of these tools lies not in any single tool, but in their combination and consistent application. A deploy script with a complete error analysis infrastructure turns post-mortems into a matter of minutes instead of hours. That lowers the barrier to more frequent deployments and increases confidence in the automation process, because everyone knows: when something goes wrong, they will know exactly what and why.

Error Analysis in Deploy Scripts: the essentials at a glance

set -x with BASH_XTRACEFD

Trace output into a separate file: exec {BASH_XTRACEFD}>>trace.log; set -x. Only enable it for DEBUG=1 to keep production clean.

trap ERR always active

ERR handler with a stack trace via BASH_LINENO and FUNCNAME. Run the context collector automatically on failure.

Structured log files

Timestamp, level, file, line number on every log line. JSON format for automated evaluation. One log file per deploy run.

Environment snapshots

Save system state at deploy start. Compare against the previous successful snapshot after a failure. The foundation for reproducible post-mortems.

11. FAQ: Error Analysis in Deploy Scripts, Reproducible and Logged

1What exactly does set -x show?
Every executed command with fully expanded variables. Prefix + for normal commands, ++ for subshells. Use BASH_XTRACEFD to send output to a separate file.
2trap ERR vs. trap EXIT?
ERR fires on a non-zero exit code, before termination via set -e. EXIT always fires on exit, whether success or failure. Combine both: ERR for diagnosis, EXIT for cleanup.
3BASH_LINENO vs. LINENO?
LINENO is the current line number. BASH_LINENO is an array of the call stack, [0] is the line of the caller. Use BASH_LINENO in ERR handlers for precise stack traces.
4Redirect all output to a log file?
exec > >(tee -a "$LOG_FILE") 2>&1 at the start of the script. Redirects stdout and stderr to both the terminal and the log file at once.
5Collect system state on error?
Call collect_context() in the ERR handler: write df -h, free -h, env | sort, ps aux into a context file. Contains the system state at the exact moment of the error.
6Make deploy errors reproducible?
Environment snapshot at deploy start. Compare against the successful snapshot after a failure. Differences are potential root causes.
7Enable set -x for just one section?
{ set -x; critical_command; set +x; } 2>>trace.log, trace only the suspect block, send output to a separate file.
8What belongs in a post-mortem?
Isolate the moment of failure, compare snapshots, reproduce the error, verify the fix in staging, extend the script with an automatic check.
9Structure log files for automated evaluation?
JSON format with timestamp, level, file, line, msg. Parsed directly by ELK and Grafana Loki. Text format as a human-readable alternative.
10Integrate error analysis into GitHub Actions?
Upload the log directory with actions/upload-artifact. On failure it is kept as an artifact for 90 days. Every failed CI deploy is fully documented.