Here’s a CI guard that looks completely correct and is completely broken:

- name: Deploy to Azure
  if: ${{ secrets.AZURE_CREDS != '' }}
  run: ./deploy.sh

The intent is obvious and reasonable: “only run this step if the Azure credentials secret is actually set.” You’d swear it works. It does not. The step either never runs, or runs when it shouldn’t, and you spend an afternoon convinced GitHub is gaslighting you.

It is. Sort of.

The crime

The secrets context is not reliably available inside a step-level if: expression. Depending on context, secrets.AZURE_CREDS evaluates to empty there regardless of whether the secret exists. So secrets.AZURE_CREDS != '' quietly resolves to '' != ''false, and your step gets skipped forever — not because the secret is missing, but because the guard couldn’t see it.

The secret is right there. It’s set. The dashboard shows it. But the if: is looking through a window with the blinds drawn. It reports “nothing here” with total confidence. An invisible secret.

This is the worst kind of bug: the thing you’re checking is fine, the check itself is the liar, and the failure is silence. No error. The step just... doesn’t happen, and you scroll past the skipped step in the logs a dozen times before you believe it.

The fix: launder the secret through env first

Bring the secret into a place the if: can see — a job-level env variable — and gate on that instead:

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      AZURE_CREDS_SET: ${{ secrets.AZURE_CREDS != '' }}   # evaluated where secrets ARE available
    steps:
      - name: Deploy to Azure
        if: env.AZURE_CREDS_SET == 'true'                 # gate on the env flag
        run: ./deploy.sh

The env: block can read secrets, so it computes a plain boolean string ('true'/'false') once, at the level where the secrets context is populated. The step then checks env.AZURE_CREDS_SET, which is just an ordinary environment variable the if: is perfectly happy to read. The secret value itself never appears in the condition — you only pass along the answer to “does it exist?”

Bonus: you never echo the secret into a log this way. You’re passing a yes/no, not the keys to the kingdom.

The moral

  • Context availability in GitHub Actions is not uniform. A variable that’s readable in one place (env:, run: with ${{ }}) may be empty in another (step-level if:). When an expression behaves like the value is missing, suspect the context before you suspect the value.
  • Compute the boolean where the data lives, gate where the data is visible. Hoist secrets-dependent logic into a job-level env flag, then branch on the flag.
  • A skipped step is a silent failure. If a step you expected mysteriously didn’t run, check its if: evaluated against what you think it did — print the flag if you have to.

The secret was never invisible. The if: just refused to make eye contact.


Amit Jethva is the CTO and co-founder of Nuvika Technologies Pvt Ltd, makers of Fintropy, a multi-cloud FinOps platform. Learn more at nuvikatech.com.