# The Invisible Secret: Why Your CI `if:` Condition Is Lying to You

_GitHub Actions, the `secrets` context, and a step that always thought it had nothing._

June 8, 2026 · 3 min · Amit Jethva

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

```yaml
- 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:

```yaml
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](https://www.nuvikatech.com/Fintropy_Overview.html), a multi-cloud FinOps platform. Learn more at [nuvikatech.com](https://www.nuvikatech.com)._
