4 pontos por GN⁺ 2025-04-09 | 2 comentários | Compartilhar no WhatsApp
  • O shell do GitHub Actions é uma configuração que define como o bloco run: será executado, mas na prática ele pode apontar não só para uma lista fixa de shells, como também para executáveis presentes no $PATH
  • Em workflows ele é opcional, mas em definições de actions é obrigatório, e o valor padrão varia conforme o runner, como bash no Linux/macOS e pwsh no Windows
  • Ao especificar algo como shell: bash, o GitHub acrescenta flags extras como bash --noprofile --norc -eo pipefail, mas o alvo executado em si pode ser um programa arbitrário
  • Se o executável não aceitar um único arquivo de entrada, é preciso incluir o argumento {0} no valor de shell; o GitHub o substitui pelo caminho de um arquivo temporário com o conteúdo do bloco run
  • Como nomes familiares como bash também são resolvidos a partir do $PATH, mudanças no $GITHUB_PATH ou executáveis falsos podem afetar como os passos seguintes serão executados

Comportamento padrão da palavra-chave shell

  • A palavra-chave shell do GitHub Actions define com qual shell um bloco run: específico será executado
  • Em workflows ela é opcional, mas em definições de actions seu uso é obrigatório
  • O shell padrão é definido de acordo com o ambiente do runner
    • Em Linux e macOS, normalmente bash
    • No Windows, normalmente pwsh

Definição explícita de shell e flags extras

  • Na documentação de defaults.run.shell, o GitHub informa que, ao declarar o shell explicitamente, também aplica flags escolhidas por ele
  • Por exemplo, ao especificar shell: bash, a execução fica assim
    • bash --noprofile --norc -eo pipefail
  • Olhando só para esse comportamento, é fácil supor que o GitHub mantém uma lista limitada de valores válidos de shell e só adiciona flags especiais a esses valores

Qualquer executável no $PATH também pode ser um shell

  • Na prática, é possível indicar em shell qualquer executável presente no $PATH
  • O GitHub executa o bloco run usando o executável especificado
  • Se esse comando não aceitar diretamente um único arquivo de entrada, é preciso passar {0} no valor de shell
    • O GitHub substitui {0} pelo caminho de um arquivo temporário
    • Esse arquivo temporário contém o conteúdo do bloco run após expansão de template

Exemplo usando C como executor de passo

  • Ferramentas que funcionam como compilador/intérprete de C também podem ser usadas para executar passos no GitHub Actions
  • O exemplo que define tcc -run {0} como shell funciona normalmente
- run: sudo apt install -y tcc
- shell: tcc -run {0}
  run: |
    #include <stdio.h>
    int main() {
      printf("Hello, world!\n");
      return 0;
    }

Exemplo mudando a resolução do shell com $GITHUB_PATH

  • Se você alterar dinamicamente o $PATH via $GITHUB_PATH, um shell: bash posterior pode apontar para um executável diferente do esperado
  • O exemplo cria um executável chamado bash no diretório atual e adiciona esse diretório ao $GITHUB_PATH
  • Depois disso, ao usar shell: bash, o GitHub pode executar o bash falso encontrado no $PATH
- run: |
    touch ./bash
    chmod +x ./bash
    echo '#!/bin/sh' > ./bash
    echo 'echo hello from fake bash' >> ./bash
    echo "${PWD}" >> "${GITHUB_PATH}"
- run: |
    echo "this doesn't do what you expect"
  shell: bash

Um ponto inesperado do ponto de vista de segurança

  • É difícil afirmar com certeza o quão importante isso é do ponto de vista de segurança
  • No GitHub Actions já existem muitos caminhos em que gravar arquivos leva à execução, e GITHUB_ENV também entra nessa categoria
  • Ainda assim, pode ser inesperado que o GitHub faça busca no $PATH até mesmo para valores de shell bem conhecidos como bash
  • Especialmente se ele injeta flags de linha de comando de acordo com o valor de shell conhecido, seria razoável esperar que bash fosse fixado em um caminho específico como /bin/bash
  • De forma mais geral, alguém pode imaginar que o GitHub só permitiria ferramentas previamente registradas no cache de ferramentas, mas o comportamento observado é o uso de executáveis vindos do $PATH

2 comentários

 
tujuc 2025-04-09

Só de ver o repositório github/runner-image, já dá para notar que há bastante pacote instalado que pode ser usado direto....

Se criar a imagem, 1 GB entra fácil....

 
GN⁺ 2025-04-09
Opiniões no Hacker News
  • Já usei a flag -x no passado para fazer o bash imprimir todos os comandos que executa em workflows do Actions, e isso ajuda bastante na depuração
    https://github.com/jstrieb/just.sh/blob/2da1e2a3bfb51d583be0...
    • Aliás, se você ativar pipefail, a etapa falha quando um dos comandos no pipe falha, mas pode não haver saída de erro, então talvez você não saiba por que falhou
      O pipefail também não evita estados de erro mais complexos. Por exemplo, numa etapa de configuração como curl ... | sudo tar ..., casos como falha na extração pelo tar, falha do sudo ou erro do shell aparecem, mas se o curl falhar no meio por um erro de rede, o tar pode deixar o binário just extraído pela metade e o shell pode encerrar imediatamente. Nesse caso não há mensagem de erro, e um executável corrompido fica no disco; se estiver ativado pular falhas, pode até haver uma tentativa de executá-lo
  • Um truque legal e privado de GitHub Actions que vi no trabalho é que dá para usar curingas no nome do evento repository_dispatch para fazer matching
    Dá para escrever algo como on: repository_dispatch: - security_scan - security_scan::*. Ao centralizar o pipeline de release, você pode forçar cada repositório a passar pelo workflow reutilizável definido e, ao enviar um evento como security_scan::$product_name::$version, fica muito mais fácil ver na aba Actions do repositório central de releases para qual produto e versão o workflow está rodando
    • Fico curioso se essa abordagem centralizada realmente funcionou bem. A organização exige que tudo seja buildado exatamente do mesmo jeito?
      Acabei de entrar numa organização tentando fazer algo parecido, mas na prática parece quase inútil. Os templates quebram com frequência e muitas vezes foram escritos assumindo um modo específico de buildar o código, sem documentação sobre como o código deve ser buildado
  • Acho que quanto menos coisas você fizer dentro do GitHub Actions, melhor
    Normalmente prefiro colocar a lógica em um sistema de build como Make e apenas chamá-lo pelo GitHub Actions, ou então escrever um pequeno programa de linha de comando e chamá-lo. Isso é muito mais fácil de depurar localmente do que no CI. É um truque interessante, mas não sei bem onde seria útil
    • Nosso workflow é basicamente make build e make test
      Depois de sermos adquiridos, olhei os arquivos de workflow da empresa compradora e vi centenas de linhas, com muitas partes repetidas. Pode me chamar de antiquado, mas quero sair da vila do YAML o mais rápido possível
    • Este artigo parece não estar recomendando que você faça isso, e sim informando que é possível
      Saber o que é possível em um sistema é útil para segurança e depuração, mesmo que você não planeje usar o recurso de fato
    • Pela minha interpretação, isso não é útil; definitivamente não é útil. Mas existe um potencial risco de segurança
      Especialmente ao “explorar” runners auto-hospedados
  • Nossa geração tremia de medo quando pediam para transformar em código uma planilha que mudava o tempo todo
    A próxima geração vai tremer quando pedirem para impor disciplina a deploys feitos com GitHub Actions
    • Estou trabalhando agora na migração de uma grande empresa para GH Actions — mais precisamente, para “pipelines baseados em YAML amarrados ao git”. Como deveria ser essa disciplina?
      Se der para explicar, acho que talvez consigamos implementá-la na nossa organização
    • Fico curioso: por que isso não teria disciplina?
  • Também dá para enganar o shell padrão, bash, para fazê-lo executar um programa arbitrário
  • Parece bem útil se as outras pessoas que leem a Action souberem o que está acontecendo
    Muitas vezes comecei com um script de shell que era quase uma transcrição de algumas linhas digitadas à mão e ele acabou virando um monstro de mais de cem linhas; nessas horas eu queria muito ter arrays e tipos de verdade, além dos recursos batteries included da biblioteca padrão do Python. Dito isso, não vou implementar a Action de build da empresa em elisp
  • O código do GitHub Actions Runner é bem fácil de ler. O lugar que define os argumentos padrão para shells/binários populares é aqui: https://github.com/actions/runner/blob/main/src/Runner.Worke...
    Ele é exposto pelo método ScriptHandlerHelpers.GetScriptArgumentsFormat. Em ScriptHandler.cs há o código de preparação do ambiente do processo e dos argumentos, e o código que de fato inicia o processo está aqui: https://github.com/actions/runner/blob/main/src/Runner.Worke...
    No geral, fiquei positivamente surpreso com a simplicidade desse código. É muito procedural e lida com inúmeras exceções, mas parece fácil de entender e depurar
  • Agora finalmente podemos usar C no CI/CD de produção e chamar isso de “trabalho de sistemas de baixo nível”
    Assembly também deve dar
  • Isso me dá esperança de que, usando goeval [1], possa ficar fácil executar código Go diretamente como tarefas de CI em arquivos YAML de workflows do GitHub
    Mas o goeval ainda não aceita entrada de arquivo diretamente, só entrada padrão, então é necessário um truque de shell. Hoje é algo como go run github.com/dolmen-go/goeval@v1 - <<'EOF' ... EOF, o que exige um pouco de boilerplate. Observação: sou o autor do goeval
    [1] https://github.com/dolmen-go/goeval
    • Não entendi por que seria necessário um “truque” de shell. go run github.com/dolmen-go/goeval@v1 - < file.go não resolveria?
    • Fui ao repositório para ver se era um programa que convertia espaços em tabs automaticamente, mas parece que ele é mais voltado a expressões de uma linha
      Nesse contexto, se a ideia era promover um projeto de brinquedo, um exemplo mais relevante do que “imprimir hello” teria poupado o clique
  • Em que o YAML do GitHub CI é melhor do que um script bash como run: pipeline.sh?
    • Há tokens da API do GitHub gerenciados automaticamente, úteis para automação de releases e publicação de artefatos e contêineres
      Dá para executar jobs ao mesmo tempo em vários sistemas operacionais e arquiteturas de CPU, além de obter informações de contexto sobre o evento que disparou o job e o estado do repositório. É conveniente para jobs por PR e automação de releases, e também pode se integrar à interface web do GitHub, por exemplo fazendo um linter marcar problemas linha a linha no PR ou renderizando falhas de teste numa página web. Também dá para usar pequenos caches para evitar baixar de novo ou rebuildar arquivos que não mudaram. O ideal é colocar o máximo possível em ferramentas comuns que rodam localmente e deixar a configuração do GitHub CI apenas com a cola necessária para gatilhos, cache e integrações com o GitHub
    • Dá para ver um resumo de cada etapa na interface do GitHub e expandir ou recolher a saída de cada uma
      Você pode aproveitar facilmente GitHub Actions criadas por outras pessoas no pipeline, modularizar workflows e controlar dependências e execução paralela. Há mais vantagens, mas o principal é não precisar implementar essas coisas por conta própria
    • Do ponto de vista do GitHub, a vantagem é que fica menos provável que os usuários migrem para outra forja Git