Português (BR)
OpsCanopy 9 min de leitura
Por que a sua condição "if" do GitHub Actions sempre roda (e como corrigir isso)
A sua condição if do GitHub Actions sempre roda como verdadeira? É a armadilha do texto literal: qualquer texto fora de ${{ }} é convertido em uma string truthy. Aqui estão a causa e a correção.
- github-actions
- ci-cd
Nesta página
- A armadilha: texto literal fora de ${{ }} é sempre truthy
- O success() implícito que desaparece quando você adiciona um if:
- success() vs always() vs failure() vs cancelled()
- Surpresas de conversão: ==, strings e a insensibilidade a maiúsculas/minúsculas
- contains e startsWith não são o mesmo que ==
- Teste o seu if: antes de fazer o push
Você adicionou um if: a um passo para que ele rodasse apenas na main, ou apenas em uma tag, ou apenas quando um passo anterior definisse uma saída. Então você fez o push — e o passo rodou mesmo assim. Toda vez. Em toda branch. A condição é apenas decoração.
Se a sua condição if do GitHub Actions não está funcionando — especificamente, se ela sempre é avaliada como verdadeira — você quase certamente caiu na armadilha mais comum de todo o produto: colocar texto literal onde o GitHub espera uma expressão. O runner não gera erro por isso. Ele silenciosamente converte o seu texto em uma string não vazia, decide que uma string não vazia é truthy e roda o passo. Este post mostra os padrões ruins exatos, as correções e as regras de conversão por trás de tudo isso, para que você pare de adivinhar.
A armadilha: texto literal fora de ${{ }} é sempre truthy
Em um if:, o GitHub já avalia o valor como uma expressão — você não envolve a coisa toda em ${{ }}. Mas no momento em que qualquer texto literal escapa para fora das chaves da expressão, o runner para de tratar a linha como uma condição e passa a tratá-la como uma string. Uma string não vazia é truthy. O seu passo sempre roda.
# BAD — the ${{ }} is embedded in a larger string, so the whole if: is a string
- name: Deploy
if: ${{ github.ref == 'refs/heads/main' }} && success()
run: ./deploy.sh
Isso parece razoável, mas o runner vê: avalie ${{ ... }} como true, depois concatene && success() como texto literal. O valor final é a string "true && success()" — não vazia, portanto truthy. O passo roda em toda branch.
A correção é escrever uma expressão sem chaves e sem texto perdido:
# FIXED — a single bare expression, no ${{ }}, no trailing literal
- name: Deploy
if: github.ref == 'refs/heads/main' && success()
run: ./deploy.sh
A mesma armadilha te pega quando você coloca a condição inteira entre aspas:
# BAD — the entire condition is a quoted string literal, always truthy
- if: "${{ steps.check.outputs.changed == 'true' }}"
run: ./build.sh
Envolver a expressão em aspas faz com que o valor do YAML seja uma string simples. O GitHub encontra um ${{ }} dentro dela, substitui o resultado e você volta a ter uma string não vazia. Remova as aspas e as chaves:
# FIXED
- if: steps.check.outputs.changed == 'true'
run: ./build.sh
Regra prática: em um if:, não há ${{ }} e não há aspas ao redor. Apenas a expressão. As chaves servem para interpolar valores em run:, name: e with: — não para condições.
Você pode colar qualquer uma dessas no Testador de Expressões e Gatilhos do GitHub Actions e vê-lo sinalizar o vazamento de texto literal antes de você fazer o push — ele avisa exatamente sobre esse padrão (está registrado como actions/runner#1173, o bug com mais reações no repositório do runner).
O success() implícito que desaparece quando você adiciona um if:
Aqui está a segunda surpresa, e é a razão de “meu passo condicional roda mesmo que o passo anterior tenha falhado”.
Todo passo e job tem uma condição success() implícita. Sem nenhum if:, um passo só roda se tudo antes dele tiver sido bem-sucedido. É por isso que os pipelines param na primeira falha sem você escrever nada.
No instante em que você adiciona um if: personalizado, esse success() implícito some. A sua condição passa a ser toda a verdade.
# BAD — you wanted "on main", but you deleted the implicit success() guard
- name: Notify on main
if: github.ref == 'refs/heads/main'
run: ./notify.sh # now runs on main EVEN IF the build above failed
Se você ainda quer que o passo exija sucesso, diga isso explicitamente:
# FIXED — re-add the success() guard you lost
- name: Notify on main
if: success() && github.ref == 'refs/heads/main'
run: ./notify.sh
É também por isso que as pessoas ficam confusas quando um passo de “limpeza” roda apenas em caso de sucesso, quando elas queriam que ele rodasse de qualquer forma — a proteção implícita ainda está lá até que elas adicionem always().

success() vs always() vs failure() vs cancelled()
Essas quatro funções de status decidem se o passo leva em conta os resultados anteriores ou não. Confundi-las é a outra metade de “meu if não se comporta”.
success()— verdadeiro apenas se todos os passos/jobs anteriores tiverem sido bem-sucedidos. (Este é o padrão implícito.)failure()— verdadeiro se algum passo anterior falhou. Use para notificações de falha.always()— verdadeiro incondicionalmente; o passo roda mesmo que um passo anterior tenha falhado ou que o workflow tenha sido cancelado. Use para limpezas que precisam sempre acontecer.cancelled()— verdadeiro apenas quando o workflow foi cancelado.
O erro clássico é combinar always() com outra condição usando && e esperar que ele ainda rode em caso de cancelamento — ele roda, mas as pessoas frequentemente querem o oposto:
# BAD — "always upload logs, but only on main" — this does NOT short-circuit on failure
- name: Upload logs
if: github.ref == 'refs/heads/main'
run: ./upload-logs.sh # skipped when the build fails, because implicit success() is gone... wait, no — it's gone, so it runs? See below.
Para ser preciso sobre esse último caso: como você forneceu um if: personalizado, o success() implícito é descartado, então o passo roda na main independentemente de o build ter passado ou não. Se você realmente quer “enviar os logs na main, com sucesso ou falha”, é exatamente isso que você tem — mas torne a intenção explícita para que o próximo leitor não fique adivinhando:
# FIXED — explicit: run on main whether the build passed or failed
- name: Upload logs
if: always() && github.ref == 'refs/heads/main'
run: ./upload-logs.sh
E para um alerta apenas em caso de falha:
# FIXED — only when something upstream broke
- name: Alert
if: failure()
run: ./page-oncall.sh
Surpresas de conversão: ==, strings e a insensibilidade a maiúsculas/minúsculas
Mesmo com expressões corretamente formadas, as regras de comparação do GitHub confundem as pessoas porque são parecidas com JavaScript, mas não são JavaScript.
O == de strings é insensível a maiúsculas/minúsculas. Isso pega as pessoas que comparam refs de branch ou valores de entrada:
# Surprise: both of these are TRUE
${{ 'MAIN' == 'main' }} # true — case-insensitive
${{ 'Refs/Heads/Main' == github.ref }} # may be true unexpectedly
Conversão flexível entre tipos. Quando os dois lados diferem em tipo, o GitHub converte em direção a um número: booleanos viram 1/0, e strings são interpretadas como números (uma string vazia e '0' são 0; strings não numéricas viram NaN, e qualquer comparação com NaN é falsa). Então:
${{ true == 1 }} # true
${{ '' == 0 }} # true — empty string coerces to 0
${{ '3.0' == 3 }} # true
${{ 'abc' == 0 }} # false — 'abc' is NaN, NaN != anything
&& e || retornam operandos, não booleanos. Assim como no JavaScript, a && b retorna b se a for truthy, caso contrário retorna a. Isso é ótimo para valores padrão (inputs.name || 'default'), mas significa que if: inputs.flag && 'yes' é avaliado como a string 'yes' — truthy — e não como um booleano limpo.
Os valores falsy são exatamente: false, 0, '' (string vazia) e null. Todo o resto — incluindo as strings 'false' e '0'… espere: '0' é falsy porque é convertido para o número 0, mas 'false' é uma string não vazia que não é convertida em número, então ${{ 'false' }} é truthy. Esse único fato causa mais bugs do tipo “minha entrada booleana é sempre verdadeira” do que qualquer outro:
# BAD — workflow_dispatch inputs are STRINGS; 'false' is truthy
on:
workflow_dispatch:
inputs:
deploy: { type: boolean }
jobs:
go:
if: inputs.deploy # with type: boolean this is fine...
# BAD — but if the value arrives as a string 'false', this always runs
- if: github.event.inputs.deploy # string 'false' is truthy!
run: ./deploy.sh
# FIXED — compare explicitly so the string is interpreted as data
- if: github.event.inputs.deploy == 'true'
run: ./deploy.sh
contains e startsWith não são o mesmo que ==
Filtrar por prefixo de ref é outro ponto em que a função errada silenciosamente faz correspondências em excesso:
# BAD — contains matches ANYWHERE, so 'feature/main-fix' passes too
- if: contains(github.ref, 'main')
run: ./deploy.sh
# FIXED — anchor to the start, or compare the full ref
- if: startsWith(github.ref, 'refs/heads/release/')
run: ./deploy.sh
# or, for an exact branch:
- if: github.ref == 'refs/heads/main'
run: ./deploy.sh
Lembre-se de que tanto contains quanto startsWith fazem a comparação de strings de forma insensível a maiúsculas/minúsculas, assim como ==.
Teste o seu if: antes de fazer o push
A razão pela qual esses bugs são tão persistentes é o ciclo de feedback: a única maneira de “testar” uma condição tradicionalmente foi fazer commit, push e ler os logs — depois adivinhar, editar e fazer push de novo. Cada suposição errada é uma ida e volta.
O Testador de Expressões e Gatilhos do GitHub Actions fecha esse ciclo. Cole a sua expressão if:, defina um contexto fictício de github / env / steps / needs e veja o resultado avaliado com as regras exatas de operador, conversão e insensibilidade a maiúsculas/minúsculas do GitHub — além de um aviso explícito quando você deixou texto literal fora de ${{ }} e acidentalmente construiu uma condição sempre truthy. Ele roda inteiramente no seu navegador; nada do seu workflow é enviado.
Se você já lançou um if: e torceu para que ele pulasse o passo, esta é a verificação que te avisa antes do runner.
Experimente o Testador de Expressões e Gatilhos do GitHub Actions →
Posts relacionados
-
8 min de leitura
Por que seu workflow do GitHub Actions não foi acionado: filtros branches, tags e paths explicados
Por que seu workflow do GitHub Actions não foi acionado: nome de branch incompatível, a semântica de AND dos filtros branches + paths, a exigência do glob **, paths-ignore no pull_request, e as correções.
-
7 min de leitura
Os erros de segurança no GitHub Actions que os linters não detectam
Validadores de YAML detectam sintaxe, não exposição. Aqui estão as cinco configurações incorretas de alto impacto do GitHub Actions — pull_request_target, injeção de script, actions sem pin, escopos amplos do GITHUB_TOKEN e curl|bash — com o padrão ruim e a correção para cada uma.
-
10 min de leitura
7 Erros Comuns no .gitlab-ci.yml (e Como Detectá-los)
Os erros de .gitlab-ci.yml que deixam os pipelines vermelhos: stages indefinidos, jobs sem script, needs e rules quebrados, uso indevido de anchors — cada um com uma correção que você pode copiar.