Skip to main content
GitHub Actions · lesson 6 of 7
GitHub Actions intermediate Lesson 6 of 7

if Conditions: Why env.DEPLOY Ran a Step It Was Meant to Skip

env.DEPLOY: false stores the string "false" — and in GitHub Actions expression syntax, any non-empty string is truthy, so if: env.DEPLOY runs the step regardless of what the flag actually says.

By SkillByExample · Published

github-actions conditionals expressions act

env.DEPLOY: false looks like it stores a boolean. It stores the string "false" — and that distinction is exactly where this bug comes from.

The bug: a “false” flag that still runs

# .github/workflows/conditional_bug.yml
name: ConditionalBug
on: [push]
env:
  DEPLOY: false
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy if env says so
        if: env.DEPLOY
        run: echo "deploying (this should NOT run)"
      - name: Always print
        run: echo "build finished"
act push -W .github/workflows/conditional_bug.yml -P ubuntu-latest=catthehacker/ubuntu:act-latest
[ConditionalBug/build] ⭐ Run Main Deploy if env says so
[ConditionalBug/build]   | deploying (this should NOT run)
[ConditionalBug/build]   ✅  Success - Main Deploy if env says so [107.731312ms]
[ConditionalBug/build] ⭐ Run Main Always print
[ConditionalBug/build]   | build finished
[ConditionalBug/build] 🏁  Job succeeded

DEPLOY is set to false — and the deploy step ran anyway. env.DEPLOY holds the string "false", not a boolean, and GitHub Actions expression syntax treats any non-empty string as truthy. "false" has four characters; it is not empty; therefore if: env.DEPLOY evaluates to true, regardless of what the string’s actual text says.

The fix: compare the string explicitly

# .github/workflows/conditional_fixed.yml
name: ConditionalFixed
on: [push]
env:
  DEPLOY: false
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy if env says so
        if: env.DEPLOY == 'true'
        run: echo "deploying (this should NOT run)"
      - name: Always print
        run: echo "build finished"
act push -W .github/workflows/conditional_fixed.yml -P ubuntu-latest=catthehacker/ubuntu:act-latest
[ConditionalFixed/build] ⭐ Run Main Always print
[ConditionalFixed/build]   | build finished
[ConditionalFixed/build] 🏁  Job succeeded

No “Deploy if env says so” step at all this time — env.DEPLOY == 'true' tests the string’s actual content ("false" does not equal "true") instead of just whether it’s non-empty.

Practice

1. Flip DEPLOY to true and confirm the fixed condition now runs the step.
env:
  DEPLOY: true
# ... if: env.DEPLOY == 'true' unchanged
[ConditionalFixed/build] ⭐ Run Main Deploy if env says so
[ConditionalFixed/build]   | deploying (this should NOT run)
[ConditionalFixed/build] ⭐ Run Main Always print
[ConditionalFixed/build]   | build finished
[ConditionalFixed/build] 🏁  Job succeeded

Now it runs — env.DEPLOY is the string "true", which does equal 'true'. Same fixed condition, correct behavior in both directions.

2. Confirm an empty string is the one case where bare truthiness testing behaves as expected.
env:
  DEPLOY_TARGET: ""
jobs:
  build:
    steps:
      - if: env.DEPLOY_TARGET
        run: echo "target is set"
      - run: echo "build finished"
[EmptyCheck/build] ⭐ Run Main echo "build finished"
[EmptyCheck/build]   | build finished
[EmptyCheck/build] 🏁  Job succeeded

No “target is set” step ran — an empty string is the one value bare truthiness testing correctly identifies as falsy. This is the legitimate use case for if: env.SOME_VAR on its own: checking “is this set at all,” not “does this hold the word true.”

3. Combine a condition with && to require two things at once.
env:
  DEPLOY: true
  ENVIRONMENT: staging
jobs:
  build:
    steps:
      - if: env.DEPLOY == 'true' && env.ENVIRONMENT == 'production'
        run: echo "deploying to production"
      - run: echo "build finished"
[Combined/build] ⭐ Run Main echo "build finished"
[Combined/build]   | build finished
[Combined/build] 🏁  Job succeeded

No production deploy step ran, even with DEPLOY: true — ENVIRONMENT is staging, not production, so the && combination correctly requires both halves to be true.

Next: caching dependencies, and measuring the real speedup it produces.

Frequently Asked Questions

Why is the string "false" truthy in a GitHub Actions if: condition?
GitHub Actions expression syntax follows JavaScript-like truthiness rules: any non-empty string is truthy, including the literal text "false". env values are always strings — YAML's env: DEPLOY: false gets stored as the string "false", not a boolean — so if: env.DEPLOY tests "is this string non-empty", which it is, regardless of what the string actually says.
How do I correctly check a boolean-flavored env variable?
Compare it explicitly against the string you expect: if: env.DEPLOY == 'true'. This tests the variable's actual content instead of just whether it's non-empty, which is the only way to distinguish the string "false" from the string "true" — both are equally truthy under bare non-empty-string testing.
Does this same trap apply to workflow inputs and outputs, not just env?
Yes — any value that flows through GitHub Actions as a string (job outputs, workflow_dispatch inputs typed as string, workflow_call inputs) is subject to the identical truthiness rule. Only values explicitly typed as boolean (an input declared type: boolean) are compared as real booleans without this gotcha.
Why didn't act or GitHub Actions raise a warning for this?
There's nothing invalid about the expression env.DEPLOY on its own — a bare variable reference in an if: condition is completely valid syntax, and "is this value truthy" is a legitimate, intentional pattern for checking whether a string is empty or set at all. The tool has no way to distinguish that legitimate use from the string-holding-a-boolean-word case that produces the surprise here.
Is there a way to actually store a real boolean in env: instead of a string?
No — env: values in a workflow file are always strings once GitHub Actions processes them, regardless of how they're written in YAML. A real boolean only exists for typed inputs (workflow_dispatch or workflow_call inputs declared type: boolean); env variables never get that treatment.