Fixando a versão do SwiftLint na CI quando o Homebrew não deixa
Esse app foi renomeado depois pra Deep Cut Atlas. Abaixo ele é chamado de “Discoverer” o tempo todo, porque era assim que se chamava no dia em que isso aconteceu.
Issue pequena, correção rápida, mas do tipo que é óbvio depois que você sabe e fácil de passar batido antes disso.
Problema
A CI do Discoverer instala o SwiftLint com brew install swiftlint antes de rodar swiftlint lint --strict. Essa linha parece completamente inofensiva, ela só está instalando uma ferramenta, mas na verdade está instalando o que quer que seja o release estável atual do Homebrew no dia em que o job roda. Meu hook de pre-commit local estava validado contra a 0.63.3. Quando fui olhar isso, o estável do Homebrew já tinha ido pra 0.65.0.
Isso significa que a CI e minha máquina local podiam estar rodando dois linters diferentes em silêncio. Se uma versão nova do SwiftLint lança regras padrão mais rígidas, ou muda como uma regra existente conta violações, uma execução de CI pode falhar sem nenhuma mudança de código do meu lado, e essa CI só roda em tags de release, então essa falha aparece bem na hora em que estou tentando lançar um build, não durante o trabalho comum do dia a dia.
Por que essa abordagem
Meu primeiro instinto foi só fixar a versão de uma formula do Homebrew, do jeito que você faria brew install node@18. Não existe pro SwiftLint, não há uma formula swiftlint@0.63.3, só a formula rolante única swiftlint que sempre acompanha a versão estável mais recente.
O SwiftLint publica sim um portable_swiftlint.zip em todo release do GitHub, um pacote de binário autocontido, sem nenhuma ligação com o ritmo de release do Homebrew. Esse é o ponto de fixação de verdade.
Implementação
- name: Install SwiftLint (pinned)
env:
SWIFTLINT_VERSION: "0.63.3"
run: |
curl -sSL -o swiftlint.zip \
"https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}/portable_swiftlint.zip"
unzip -q swiftlint.zip -d "$RUNNER_TEMP/swiftlint"
echo "$RUNNER_TEMP/swiftlint" >> "$GITHUB_PATH"
Baixa o asset da versão exata, descompacta em algum lugar no espaço temporário do runner, adiciona esse diretório ao PATH. Sem sudo, sem tocar em diretórios do sistema. Escolhi a 0.63.3 especificamente porque é a versão contra a qual eu já vinha rodando esse código-base o tempo todo, fixar na mais nova só trocaria um tipo de deriva por outro, exceto que agora seria uma deriva que eu ainda não checei contra minha própria configuração de lint.
Enquanto estava no arquivo de CI, também adicionei -resultBundlePath ao passo de teste e um passo actions/upload-artifact condicionado a if: failure(). Antes disso, uma execução falhando disparada por tag me deixava só com o log de console cru pra descobrir o que quebrou. Agora o bundle xcresult completo, detalhe de assertion, screenshots se um teste de UI falhar, sobe automaticamente, mas só quando algo de fato falha, então isso não polui toda execução com sucesso com um artefato desnecessário.
Pegadinhas
O lado local disso não pode ser forçado do mesmo jeito sem adicionar atrito de verdade. Ainda não existe uma formula do Homebrew versionada, então a instalação local de um colaborador continua “o que estiver atual” a menos que ele também vá baixar um binário portátil na mão, não vale o custo de setup pro que hoje é um projeto solo. Em vez disso só deixei um comentário de uma linha ao lado da linha brew install swiftlint do README apontando pra versão exata que a CI fixa, então se algum dia eu atualizar uma, lembro de atualizar a outra de propósito em vez de por acidente.
O actions/upload-artifact também acabou já estando na v7, eu teria chutado v4 de memória, o que é um bom lembrete pra de fato checar o release atual de uma ferramenta em vez de confiar no que “soa atual”.
Resultados
Disparei o workflow manualmente contra a branch antes de fazer merge (a CI desse repositório é só por tag/dispatch, não a cada push, pra controlar custo de runner macOS) e confirmei que swiftlint --version no log da CI imprimiu exatamente 0.63.3, não o que quer que fosse o estável do Homebrew naquele dia. Correção pequena, mas fecha uma lacuna real: a execução de CI exata que mais importa (uma tag de release) era a mais exposta a um bump de dependência não revisado quebrar por motivos que não têm nada a ver com o código de fato sendo lançado.
Leitura relacionada
Uma seção de tracklist, e por que levou 30 minutos
Um método de protocolo, um enum de estado reaproveitado, uma convenção de roteamento que se manteve, e um orçamento de lint que forçou uma divisão que valia a pena fazer de qualquer forma.
Configurando SwiftLint e CI do GitHub Actions para um app iOS (e o runner que mentiu)
Adotando um linter em um código que nunca teve um, um hook de pre-commit sem framework, e três coisas que o runner macOS fez que a documentação não mencionava.
A playlist que já tinha o nome certo
Um artefato de renomeação, uma API sem campo de autor pra atualizar, e um teste em dispositivo real provando que o bug já tinha se corrigido sozinho, fechado como aceitar como está.
Você também pode achar útil
Proton Mail
E-mail criptografado de ponta a ponta, com arquitetura de acesso zero.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba maisNordPass
Gerenciador de senhas da equipe por trás da NordVPN, com um plano gratuito.
Como afiliado da NordPass, ganho com compras qualificadas.
Saiba maisRackNerd VPS
Hospedagem VPS econômica para serviços leves que funcionam continuamente.
Como afiliado da RackNerd, ganho com compras qualificadas.
Saiba mais