DEV Community

EL E
EL E

Posted on

A Command That Matched Zero Files Still Exits 0: How to Catch Silent CI Failures

A Command That Matched Zero Files Still Exits 0: How to Catch Silent CI Failures

Every engineer has felt the cold dread of pushing a critical fix, watching the CI pipeline turn gloriously green, and then discovering in production that the deployment script didn't touch a single file because a glob pattern quietly evaluated to nothing.

The pipeline passed. The logs looked clean. But your code was never updated.

Why does this happen? In standard shell scripting and CI environments like GitHub Actions, commands like grep, find, or custom glob expansions frequently exit with status 0 even when they match zero files or process zero items. To a build system, "no errors encountered while running the binary" is indistinguishable from "successfully processed your target files."

In this article, we examine why silent zero-match failures are a silent killer of CI reliability, and how you can bulletproof your automation pipelines using strict assertion wrappers and open-source verification tools.


The Root of the Problem: Exit Code Ambiguity

Let's look at a typical cleanup or linting step in a CI pipeline:

find src/ -name "*.legacy.js" -delete
Enter fullscreen mode Exit fullscreen mode

If src/ contains no files matching *.legacy.js, what does find return?
It exits with 0.

To the shell, find executed successfully: it searched the directory, encountered no permission errors or syntax issues, and completed its execution. It has no concept of your intent that matching files ought to exist.

Now scale this to a deployment workflow:

sed -i 's/OLD_VERSION/NEW_VERSION/g' dist/**/*.bundle.js
Enter fullscreen mode Exit fullscreen mode

If your build step changed the output directory structure or filename extensions, dist/**/*.bundle.js might expand to a literal string or match nothing. sed might fail or do nothing, but depending on how the shell handles globbing (especially with nullglob disabled), the command might either fail or silently succeed with zero modifications. If your CI script uses set -e, it only catches non-zero exit codes. A zero exit code gives your pipeline a false sense of security.


Real-World Scenarios Where Zero-Match Slips Through

1. File Migration and Refactoring Scripts

During large-scale refactors, paths move around. If a CI maintenance script relies on relative paths to clean up temporary artifacts or migrate configuration files, a broken path changes the target from "all configuration files" to "nothing at all."

2. Security Audits and Secret Scanning

Imagine running a custom pre-commit hook or CI check to ensure no hardcoded private keys or tokens slipped into staging directories:

grep -rnw 'configs/' -e 'BEGIN PRIVATE KEY'
Enter fullscreen mode Exit fullscreen mode

If configs/ was accidentally excluded or renamed in a recent PR, grep finds nothing and exits with 1 (no match found). But what if you wrappered or piped it incorrectly, or if the directory path was subtly wrong? In many complex pipeline scripts, exit codes from greps are swallowed by pipes or conditional wrappers (|| true), turning an intended security gate into a ghost barrier.


How to Fix It: Defensive Shell Scripting

To stop silent zero-match failures, you must enforce strict assertions on file operations before and after execution.

Approach 1: Pre-Check File Counts

Before running a batch operation, assert that your file list or glob expansion actually contains items:

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

files=( src/**/*.ts )
if [ ${#files[@]} -eq 0 ] || [ ! -e "${files[0]}" ]; then
  echo "Error: Expected TypeScript files to process, but found zero matches." >&2
  exit 1
fi

echo "Processing ${#files[@]} files..."
# Proceed with processing
Enter fullscreen mode Exit fullscreen mode

Approach 2: Using Dedicated Verification Tools

For robust production pipelines, writing custom bash guards for every glob pattern becomes tedious and error-prone. This is why teams adopt specialized verification wrappers like zero-match-guard or offchain-integrity-verifier.

For instance, using zero-match-guard in your CI workflow ensures that any file operation or glob expansion explicitly fails the build if the match count falls below an expected threshold:

- name: Verify Migration Targets
  uses: elwakeupman-shhh/zero-match-guard@v1
  with:
    path: "dist/**/*.bundle.js"
    min-matches: 1
Enter fullscreen mode Exit fullscreen mode

If zero files match the pattern, the action halts immediately with a clear diagnostic message, preventing broken artifacts from reaching downstream deployment stages. Similarly, for validating build integrity without heavy external dependencies, lightweight verifiers like offchain-integrity-verifier verify checksums and file presence locally in a single zero-dependency pass.


Conclusion

Silent CI failures are insidious because they rob you of visibility when things go wrong. By recognizing that exit code 0 only means "no command execution error" rather than "operation successfully acted on your expected targets," you can write significantly more resilient automation scripts.

Take a look at your CI pipelines today: are your file-processing steps protected against zero-match silent passes? Add explicit count checks or integrate guard actions like zero-match-guard to ensure your builds never lie to you.

Top comments (0)