Skip to content

Type-Safe CI Logic

Continuous integration pipelines that silently do the wrong thing are among the most hazardous sources of failure in automated release systems. When a job intended to act as a safety gate passes with a "green" status but fails to perform its actual function, it erodes trust and can lead to accidental deployments. This often stems from a mismatch between the data types declared in configuration and the comparison logic used in the scripts that consume that data.

A Skipped or Mis-flagged Job Is a Green Job

The most dangerous CI failures don't raise errors. They simply pass, having done the wrong thing or nothing at all. A deployment script that intends to run on a release but is fed a mis-interpreted flag may simply publish a development build instead, reporting success. Nothing fails, but the release never happens.

Anatomy of a Silent Failure

A common failure pattern involves boolean flags passed as inputs to reusable workflows. In one incident, a central build-and-publish workflow was designed to be triggered by an upstream release-creation process. The calling workflow would pass an input, is_release_candidate, to signal whether the current build should be published as a formal release or a pre-release artifact.

The release-automation tool correctly calculated the value and passed it to the build workflow. However, the build workflow's logic contained a subtle but critical flaw:

# From the downstream, consuming workflow
if: inputs.is_release_candidate == true
  # ... logic to publish a formal release

The problem was that the is_release_candidate input was declared with type: string. The calling workflow was passing the string "true" or "false". The condition inputs.is_release_candidate == true compared the string "true" against the boolean true, which will always evaluate to false in this context. As a result, the formal release logic was never executed. The workflow would proceed, fall through to the next step, and publish a development build instead. The job completed successfully, but the intended release was never published, and no alert was raised.

Configuration Inputs Are Often Strings

Many CI/CD systems serialize all workflow inputs into strings, regardless of their original type. A boolean true in a calling workflow often becomes the string "true" in the downstream job. This puts the responsibility on the consuming script to handle the value correctly. The logic must anticipate a string and compare it to a string.

The fix for the silent failure was simple: treat the input as the string it is.

# Correct comparison for a string input
if: inputs.is_release_candidate == 'true'
  # ... logic to publish a formal release

This ensures the comparison is between two values of the same type. A small table summarizes the correct approach for different contexts within a typical CI workflow file:

Context Declaration Correct Comparison
Workflow Expression type: boolean inputs.my_flag == true
Workflow Expression type: string inputs.my_flag == 'true'
run: (Shell Script) type: string [ "${{ inputs.my_flag }}" == "true" ]

This explicit-comparison principle is the first line of defense against type coercion bugs.

The Perils of "Truthy" Evaluation in Scripts

When these string-based values are passed from the CI runner's environment into shell scripts or other interpreted languages, the problem gets worse. Many languages have a concept of "truthiness," where non-empty values are automatically coerced to true in a boolean context.

Consider a shell script inside a run: block:

# WARNING: This is a common bug
if [ "${{ inputs.is_release_candidate }}" ]; then
  echo "Release candidate detected, running release steps..."
  # This will run for BOTH "true" and "false"
fi

In Bash, the [ string ] test checks if the string is non-empty. Since both "true" and "false" are non-empty strings, this condition will pass in both cases. A flag explicitly set to false to prevent a release would instead be interpreted as a signal to proceed.

The same issue exists in other common CI scripting languages:

  • Python: bool("false") evaluates to True.
  • JavaScript: Boolean("false") evaluates to True.

This behavior, where "false" means true, is a recipe for unintended consequences. Automation logic must never rely on implicit truthiness for flags.

Enforcing Strictness in Comparisons

To build robust CI logic, always perform explicit, type-aware comparisons.

  • Check for the literal string: Instead of checking for existence, check for the exact value.
# Correct way to check in shell
if [ "${{ inputs.is_release_candidate }}" == "true" ]; then
  echo "Release candidate detected, running release steps..."
fi
  • Use type-strict operators: When available, use operators that prevent type coercion, such as JavaScript's ===.

  • Normalize values early: A good practice is to read the string input once at the beginning of a script, convert it to a true boolean, and use that variable throughout. This contains the "unsafe" string handling to a single, explicit block.

IS_RELEASE=false
if [ "${{ inputs.is_release_candidate }}" == "true" ]; then
  IS_RELEASE=true
fi

# ... later in the script
if [ "$IS_RELEASE" == true ]; then
  # ... safe, explicit logic
fi

Defensive Parameter and Schema Definition

While scripts must be defensive, the workflow definition itself can help. When a CI system allows it, define the type of each input. Setting type: boolean for a flag makes the runner responsible for passing a proper boolean, simplifying the consuming logic.

However, if you cannot control the type (e.g., in systems that default all inputs to strings), the responsibility falls to the documentation and the implementation. The description of the input should explicitly state that it is a string representation of a boolean (e.g., "true" or "false") and must be handled as such.

Auditing for Weak Logic

The silent failure involving is_release_candidate was discovered during a broad, cross-repository audit of CI workflow patterns. This highlights the need for proactive review. It is not enough to fix these issues as they are discovered; teams should periodically audit their automation logic for these specific anti-patterns:

  • Vague if [ "$VAR" ] checks in shell scripts.
  • Use of type-coercing equality operators (== in JavaScript, for example) on configuration inputs.
  • Workflow if: conditions that compare a variable to a bare true or false without confirming the variable's type.
  • Missing default: values for workflow inputs, which can lead to null/empty strings that have their own truthiness behaviors.

By treating CI/CD logic with the same rigor as application code. This means enforcing strict types and explicit comparisons. Doing so allows us to build more reliable, predictable, and safe automation pipelines.

Comments