- Ao trazer de volta para o GitHub Actions uma CI complexa, merge queue, vários runners, build de Rust, imagens Docker e testes de integração se entrelaçaram, e o custo de depuração acabou ficando mais evidente do que o da configuração
- Todas as mudanças que entram na
main precisam passar em todos os testes, e erros pequenos como formatação, dependências não usadas e lint são corrigidos automaticamente
- Para forçar tanto a validação antes quanto depois da merge queue, foi preciso deixar os nomes dos jobs idênticos nas duas etapas; caso contrário, os checks podem travar ou mudanças com falha podem ser mescladas
GITHUB_TOKEN, permissions do workflow, tokens customizados e exceções envolvendo fork e self-hosted runner se misturam, tornando o modelo de segurança difícil de entender e propenso a erros
- Executar contêineres Docker, manter workflows em YAML e testar localmente de forma limitada desacelerou o desenvolvimento, mas o novo script de CI reduziu bastante o tempo de merge
Uma CI complexa de volta ao GitHub Actions
- Nas últimas duas semanas, os scripts de CI foram reescritos novamente em GitHub Actions
- Esta é a terceira reformulação da configuração de CI
- Primeiro era GitHub Actions
- Depois migrou para Earthly
- Como o Earthly foi descontinuado, voltou para o GitHub Actions
- Hoje a CI lida ao mesmo tempo com merge queue, vários runners, build de Rust, imagens Docker e testes de integração pesados
- Os runners incluem self-hosted, blacksmith.sh e GitHub-hosted
- Para mesclar um único PR, são consumidas cerca de 1 hora de CI em vários runners paralelos
Condições que a CI precisa atender
- Toda mudança que entra na
main precisa passar em todos os testes
- Erros pequenos, como formatação, dependências não usadas e problemas de lint, não devem terminar em falha, e sim ser corrigidos automaticamente
- Os artefatos testados na CI precisam ser os mesmos artefatos reais de release
- Para uma boa experiência de desenvolvimento, a CI precisa terminar rápido
- O GitHub Actions oferece suporte técnico a esses requisitos, mas o processo de configuração vem acompanhado de armadilhas ocultas, comportamentos inconsistentes e depuração difícil
Merge queue e status checks
- A chave para manter a branch
main limpa é a merge queue do GitHub
- A merge queue faz rebase do PR sobre a
main antes da execução da CI
- A execução necessária da CI se divide em duas etapas
- Antes de entrar na fila, a CI roda para corrigir automaticamente problemas pequenos
- Dentro da fila, a CI roda de novo para validar o estado final da mesclagem
- Para tornar obrigatórias as duas execuções no GitHub Actions, foi necessário definir o mesmo nome de job nas duas etapas
- Assim, o GitHub trata as duas execuções como o mesmo check, e a mesclagem só é permitida se ambas tiverem sucesso
- Essa solução só foi encontrada depois de horas de depuração e de uma resposta no Stack Overflow
- Outras abordagens podem fazer com que o status check fique aguardando antes da entrada na fila e o job nem comece, ou permitir que um job que deveria falhar dentro da merge queue ainda assim resulte em mesclagem
O modelo de segurança difícil de entender do GitHub Actions
- Depois do caso recente em que uma GitHub Action popular foi comprometida, surgiu a recomendação de “fixar dependências por hash”, mas os comentários mostraram que quase ninguém faz isso
- O GitHub Actions tem um token padrão chamado
GITHUB_TOKEN
- Esse token é inicializado com permissões padrão
- As permissões padrão podem ser definidas em Actions → General → Workflow Permissions nas configurações do repositório
- Se as permissões padrão do
GITHUB_TOKEN forem restritas, é preciso elevá-las para executar actions e comandos necessários; se forem permissivas, é possível remover algumas permissões no arquivo de workflow
- Um padrão melhor seria começar com nenhuma permissão e deixar o usuário adicionar apenas o necessário
- Há muitos tipos de permissão, e, para quem não é especialista em GitHub, é difícil entender o que exatamente cada uma protege
Exceções de token e permissão
- Ao gerar releases do GitHub automaticamente com
softprops/action-gh-release, é usado um token customizado chamado CI_RELEASE
- name: Release on GitHub
if: env.version_exists == 'false'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ env.CURRENT_VERSION }}
generate_release_notes: true
make_latest: true
token: ${{ secrets.CI_RELEASE }}
- Com o token padrão, a release em si até é concluída, mas workflows posteriores não são disparados
- Como não há indicação separada disso, foi preciso encontrar uma issue de alguém com o mesmo problema para entender a causa
- Também é possível elevar permissões dentro do YAML do workflow
- A ideia de elevar permissões dentro do próprio código que se quer proteger soa estranha
- Segundo a documentação do GitHub, a chave
permissions pode adicionar ou remover permissões de leitura para repositórios forkados, mas normalmente não permite conceder permissões de escrita
- A exceção é quando um administrador ativa a opção Send write tokens to workflows from pull requests nas configurações do GitHub Actions
- Com tantas exceções e armadilhas, o modelo de segurança do GitHub Actions é poderoso, mas também amplia a superfície de ataque e a possibilidade de erro
A incerteza do self-hosted runner
- A documentação do GitHub não recomenda o uso de self-hosted runner em repositórios públicos
- Isso porque forks de repositórios públicos podem executar código perigoso na máquina do self-hosted runner por meio de PRs
- O GitHub também tem uma configuração de self-hosted runner que exige aprovação para executar PRs de contribuidores externos
- A documentação não dá uma resposta clara sobre se usar essa configuração junto com self-hosted runner é realmente seguro, e também não há consenso na internet
- O nível de complexidade é alto demais para chegar a 100% de confiança
O choque entre Docker e GitHub Actions
- O GitHub Actions pode executar jobs dentro de contêineres
- A vantagem é poder empacotar previamente dependências em um dev container, sem precisar instalá-las toda vez
- No uso real, problemas de permissão de arquivo apareceram repetidamente
- O contêiner faz o build dos arquivos como um usuário, mas o GitHub runner pode rodar com outro uid/gid
- Como resultado, pode não ser possível acessar arquivos dentro do contêiner, no workspace do GitHub ou em diretórios temporários do host
- O diretório
$HOME também pode ficar desalinhado
- O dev container pode instalar ferramentas em
/home/ubuntu
- Dentro do GitHub Actions, o
$HOME pode ser /github/home
- Ferramentas que dependem de arquivos sob
$HOME podem não encontrar o que precisam
- Actions que interagem com o sistema host também podem quebrar
- O cache do GitHub é limitado a 10GB, então foi usada a sticky disk action da blacksmith para montar um drive NVMe para cache
- Isso não funcionava dentro do contêiner, e só foi resolvido depois de uma correção da blacksmith.sh
- O próprio campo
container também tem limitações
- Não é possível sobrescrever o entrypoint
- Também não dá para executar apenas alguns steps dentro do contêiner e o restante fora dele
Desenvolvimento e depuração de workflows em YAML
- A lógica do GitHub Actions é escrita em YAML, e, quanto mais complexa fica, mais fácil é errar
- A inspeção do linter de GitHub YAML no RustRover ajudou, mas ainda falta uma análise estática melhor
- Localmente, é difícil testar de forma suficiente o comportamento real da CI
- O act é uma ferramenta conhecida, mas só suporta uma pequena parte do que se queria fazer na CI
- A melhor forma de depuração acabou sendo criar outro repositório idêntico e repetir
git commit -a -m "wip" && git push test-ci branch até a CI se comportar como esperado
Separação de workflows e reutilização de artefatos
- Para não rodar o pipeline inteiro de CI toda vez, cada workflow foi mantido pequeno
- Ao final de cada workflow, os artefatos são enviados, e workflows posteriores os baixam para evitar reconstruir tudo do zero
- Também é possível baixar artefatos de execuções anteriores para testar workflows de forma isolada
- Porém, ao baixar de uma execução anterior, ainda é preciso fornecer um token à action
download-artifact
- Esse token pode ser o token padrão, mas por que ainda precisa ser informado explicitamente continua sendo uma dúvida sem resposta
- O arquivo principal de workflow acaba virando uma cadeia que chama outros arquivos YAML
jobs:
invoke-build-rust:
name: Build Rust
uses: ./.github/workflows/build-rust.yml
invoke-build-java:
name: Build Java
uses: ./.github/workflows/build-java.yml
invoke-tests-unit:
name: Unit Tests
needs: [invoke-build-rust, invoke-build-java]
uses: ./.github/workflows/test-unit.yml
invoke-tests-adapter:
name: Adapter Tests
needs: [invoke-build-rust]
uses: ./.github/workflows/test-adapters.yml
secrets: inherit
invoke-build-docker:
name: Build Docker
needs: [invoke-build-rust, invoke-build-java]
uses: ./.github/workflows/build-docker.yml
invoke-tests-integration:
name: Integration Tests
needs: [invoke-build-docker]
uses: ./.github/workflows/test-integration.yml
invoke-tests-java:
name: Java Tests
needs: [invoke-build-java]
uses: ./.github/workflows/test-java.yml
- Alguns jobs precisam de
secrets: inherit
- Quando um workflow chama outro workflow, os secrets não são compartilhados por padrão
- Esse era o motivo de a falha acontecer no pipeline completo de CI, mas não na execução de steps individuais
Merges mais rápidos, depuração ainda cara
- O novo script de CI reduziu bastante o tempo de merge, e o resultado agradou
- Ainda assim, foi preciso gastar tempo demais para chegar a esse ponto, e a depuração deveria ser bem mais fácil quando problemas aparecem
Ainda não há comentários.