GitHub Actions if: always() Runs a Step Even After Cancellation
Status-check functions in if: do not behave the way they read. always() also runs when a run is canceled, and adding any if: removes the implicit success() gate, so a step you meant to gate now runs in states you did not expect.
What this error means
A cleanup or notification step runs even when the workflow was canceled, or a step runs after a previous failure because adding an if: silently dropped the default "only on success" behavior.
- run: ./upload-logs.sh
if: always() # also runs when the job is CANCELED, not just on failure
# wanted: run on success or failure, but not on cancelCommon causes
always() includes the canceled state
always() means run regardless of status - including cancellation. To run after success or failure but skip cancellation, use !cancelled() instead.
Any if: drops the implicit success() gate
Without an if:, a step only runs if prior steps succeeded. The moment you add any if: (for example if: github.ref == ...), that default gate is gone, so the step can run after a failure unless you re-add success().
How to fix it
Use the function that matches the states you want
Combine status functions explicitly so the step runs in exactly the intended states.
# run on success or failure, but NOT on cancel
- run: ./notify.sh
if: ${{ !cancelled() }}
# run only when something failed
- run: ./collect-debug.sh
if: failure()Re-add success() when you add another condition
- When gating on a branch/event, keep the success gate: if: success() && github.ref == 'refs/heads/main'.
- Use failure() for steps that should only run after an earlier failure.
- Reserve always() for steps that must run even when the run is canceled.
How to prevent it
- Prefer !cancelled() over always() unless you truly want cancel included.
- Remember adding any if: removes the implicit success() gate - re-add it if needed.
- Use failure()/success() explicitly for conditional cleanup and notifications.