Pare de escrever workflows em YAML
Como eu me frustrei tanto com GitHub Actions que acabei criando meu próprio executor de comandos.
Nota:
nowinclui algumas contribuições de LLMs, todas elas revisadas com cuidado por um humano.
Se você já teve que lidar com GitHub Actions, você provavelmente esteve na mesma situação que eu. Você escreve (ou, muito provavelmente, copia e ajusta) um workflow, faz o push da branch e torce que tudo rode como esperado. Se você tiver sorte, você vai pegar um erro fácil de corrigir, como uma variável faltante no seu ambiente. Senão… lá se vai o resto do seu dia útil.
O simples fato de termos máquinas de CI gratuitas para uso livre foi uma grande bênção para vários projetos de código aberto, mas não tem nada mais doloroso do que o ciclo de iteração lento para conseguir fazer tudo funcionar da maneira exata. Entre as principais reclamações, a falta de ferramentas para validar workflows localmente ou como você é propenso a ataques de supply chain por ações maliciosas são algumas das comumente levantadas.
Mas minha maior queixa com as GitHub Actions seria o fato de que você as declara em YAML puro.
Uma DSL feita às coxas
YAML se apresenta como uma linguagem de serialização amigável a humanos, mas minha experiência é longe disso. Comparada a TOML (que é mais adequada a arquivos de configuração) e JSON (para dados consumidos por sistemas), qualquer arquivo YAML de tamanho razoável rapidamente se torna uma bagunça de tipos de dado mistos sensível a espaços, sem quaisquer parênteses coloridos da sua IDE para se localizar.
Mesmo que o formato fosse decente o bastante para não confundir strings com outros tipos, ele realmente não foi feito com algo como workflows das GitHub Actions em mente. Elas forçam o formato aos seus limites com condições, variáveis matriciais e comunicação entre tarefas, sem falar de um sistema de permissionamento que permite tudo por padrão, ou das várias armadilhas que você só descobre usando um linter decente como Zizmor – que pelo menos tem o bom senso de pensar no uso local antes de tudo.
Adicione a isso que a maneira de criar suas próprias ações na prática é colando vários blocos de código shell juntos no YAML, ou de forma imperativa com Node.JS, e você acaba de obter um sistema complexo, feito a partir de blocos opacos abstraindo o que não deveria ultrapassar dois ou três linhas de CLI.
Eu me frustrei tantas vezes que acabei pensando que “deve ter uma maneira melhor de fazer isso”. E como eu já estava inteirado a fundo em Nix, eu decidi conectar essa ferramenta ao meu problema e formular uma solução.
now sei o que vem agora
Inicialmente, decidi atacar algumas dessas questões: o fato de que workflows não usam uma linguagem de programação real e de que não é possível rodá-los localmente. Adicione a isso o superpoder de Nix em criar ambientes reproduzíveis com uma pitada da estrutura de três camadas das GitHub Actions (workflows, tarefas e passos), e você acaba criando now.

Embora tenha sido criado originalmente como um executor de CI alternativo – com funcionalidades como uso de executores Nix remotos, diferente estratégias de checkout e manuseio de segredos –, now é igualmente poderoso quando utilizado como um executor de comandos local. Parseando entradas em Nix e deixando toda a orquestração para o cópdigo em Rust (com Zig no meio para scripts pequenos sem dependências), ele possibilita definir workflows local-first que podem rodar em qualquer lugar em que você pode instalar Nix.
Um exemplo completo
Aqui está um gostinho do que um workflow now.nix complexo parece:
{ runner, ... }: {
# `now run` sem argumentos irá rodar esta tarefa por padrão
default = [ "push-docker" ];
# Nós declaramos cada tarefa como atributos de `jobs`
jobs = {
build-docker =
# Nós podemos especificar vários construtores/executores com `runner.matrix`
runner.matrix
[
{
# Rode num sistema Linux x86_64 com Docker
pkgs = import <nixpkgs> { system = "x86_64-linux"; };
requiredSystemFeatures = [ "docker" ];
}
{
# Também rode num sistema Linux aarch64 com Docker
pkgs = import <nixpkgs> { system = "aarch64-linux"; };
requiredSystemFeatures = [ "docker" ];
}
]
(
# Cada executor ganha uma instância de `nixpkgs` apropriada
{ pkgs, ... }: {
name = "Build Docker (${pkgs.stdenv.hostPlatform.system})";
steps = [
# Nós podemos fazer upload de derivações Nix ao orquestrador
# (incluindo de imagens Docker construídas com `dockerTools`)
(runner.steps.upload {
name = "docker-${pkgs.stdenv.hostPlatform.system}";
deriv = pkgs.dockerTools.buildLayeredImage {
name = "hello";
tag = "latest";
config.Entrypoint = [ (lib.getExe pkgs.hello) ];
};
})
];
}
);
push-docker = { pkgs, ... }: {
name = "Push Docker";
# Declare uma dependência explícita entre tarefas
needs = [ "build-docker" ];
steps = [
{
name = "Log in to DockerHub";
env = {
DOCKERHUB_PUSH_TOKEN = runner.secret "DOCKERHUB_PUSH_TOKEN";
DOCKERHUB_USERNAME = runner.var "DOCKERHUB_USERNAME";
};
path = [ pkgs.docker ];
# Entre no Docker Hub com nossas credenciais e saia ao terminar
run = ''
echo "$DOCKERHUB_PUSH_TOKEN" | docker login \
--password-stdin --username $DOCKERHUB_USERNAME docker.io
'';
teardown = ''
docker logout docker.io
'';
}
{
name = "Log in to GHCR";
env = {
GITHUB_TOKEN = runner.secret "GITHUB_TOKEN";
GITHUB_USERNAME = runner.var "GITHUB_USERNAME";
};
path = [ pkgs.docker ];
# Entre no GHCR com nossas outras credenciais
run = ''
echo "$GITHUB_TOKEN" | docker login \
--password-stdin --username $GITHUB_USERNAME ghcr.io
'';
teardown = ''
docker logout ghcr.io
'';
}
{
name = "Push images";
env = {
# Nós podemos criar Docker tags separadas por espaço com Nix
TAGS = builtins.concatStringsSep " " (
map ({ image, tag }: "${image}:${tag}") (
lib.cartesianProduct {
image = [
"docker.io/${runner.var "DOCKERHUB_USERNAME"}/hello"
"ghcr.io/${runner.var "GITHUB_USERNAME"}/hello"
];
tag = [
"latest"
"main"
];
}
)
);
AMD_IMAGE = runner.download "docker-x86_64-linux";
ARM_IMAGE = runner.download "docker-aarch64-linux";
};
# Declare todas as dependências para esse passo
path = [
pkgs.docker
pkgs.docker-buildx
pkgs.skopeo
];
run = ''
for TAG in $TAGS; do
skopeo copy docker-archive:$AMD_IMAGE "docker://$TAG-amd64"
skopeo copy docker-archive:$ARM_IMAGE "docker://$TAG-arm64"
docker buildx imagetools create \
--tag "$TAG" "$TAG-amd64" "$TAG-arm64"
done
'';
}
];
};
};
}
Se você tem familiaridade com a linguagem Nix, o código acima deve ser intuitivo, embora seja convoluto. Nós declaramos duas tarefas, build-docker e push-docker, com uma dependência explícita entre elas via needs = [ "build-docker" ]. A primeira cria uma imagem Docker (como uma derivação Nix, é claro) em ambas x86_64 e aarch64, enquanto a segunda se loga nos registries de Docker e faz push das nossas imagens.
Algumas diferenças das GitHub Actions que chamam a atenção:
- Especificar workers de arquiteturas diferentes é tão simples quanto instanciar
nixpkgspara cada um. Além disso, nós podemos definirrequiredSystemFeaturespara apenas utilizar hosts que disponibilizam um daemon Docker. runner.steps.uploadconstrói e compartilha nossas imagens Docker, que podemos resgatar depois comrunner.download.- Nós declaramos nossas variáveis de ambientes e pacotes em cada passo. Um passo pode receber
pkgs.dockere uma variável secreta, enquanto que o próximo pode apenas utilizarpkgs.skopeo. Isso é similar a criar shells de desenvolvimento para cada passo, com segredos sendo compartilhados apenas aonde são declarados. - Nós podemos usar programação funcional de verdade! Isso permite que realizemos todo tipo de operação nós mesmos ao invés de depender de ações externas.
- Limpeza de ambiente é uma funcionalidade de primeira classe via scripts
teardown.
O modelo de mundo do Nix se encaixa perfeitamente no caso de uso de CI declarativo. Toda a comunicação ocorre através da camada de orquestração ou pelo Nix store, via derivações. E tudo o que você precisa para rodar executores remotos é uma conexão SSH a uma máquina rodando Nix.
Now para sempre
Isso é apenas a superfície; now está cheio de funcionalidades úteis. Aqui estão algumas:
- Ao invés de depender exclusivamente de bash, você pode especificar scripts em qualquer shell que você quiser, seja
nushelloupython3ou até mesmorust-script! - Sandboxing permite que você rode passos com permissões restritas e granulares, tanto em Linux quanto macOS.
- Você pode usar estratégias de checkout diferentes dependendo de se o diretório atual deve ser copiado para executores remotos ou não.
- Priorizando o uso local,
nowtem múltiplas maneiras de verificar seu workflow antes de você ter que rodar qualquer coisa. - Com suporte a shebang, você pode distribuir workflows rodáveis como um único arquivo
now.nix. - Como um executor de comandos poderoso e extensível, ele inclusive tem suporte a rodar dependências de desenvolvimento como PostgreSQL com seu add-on de serviços.
- Você pode facilmente invocar
nownas suas GitHub Actions, tornando trivial de ligar no seu CI atual.
Uma ressalva é de que now dependende de um modelo “traga suas próprias máquinas”, necessitando de uma conexão SSH a elas – o que não condiz muito com o modelo de executores das GitHub Actions. Isso quer dizer que ele não é um substituto um-pra-um delas (a não ser que você descubra como integrá-lo com o upterm; nesse caso, me avise! embora você consiga resultados bem decentes com Tailscale SSH às custas de ergonomia). Mas para meus casos de uso, é mais do que o bastante para preencher as lacunas que as GHA criam.
Apesar dos tempos de inicialização lentos devidos a avaliação do Nix e construção de derivações, ele acaba também sendo um bom substituto para ferramentas como just, em especial quando você precisa de várias ferramentas em uma versão específica e reproduzível. Aqui estão algumas das coisas para as quais eu uso o now:
- Configurar repositórios.
- Fazer deploy de websites construídos com Nix a servidores.
- Rodar a própria suíte de testes do
now. - Tarefas de formatação, linting e testes do Sandhole.
- Fazer push de imagens Docker cross-compiled a um Raspberry Pi.
- Criar AppImages em um ambiente compatível com FHS.
now está disponível sob a licença AGPL-3.0. Se ele soa como algo que pode resolver um dos problemas que você tem com GitHub Actions ou outros executores de comandos, leia a documentação e experimente!█
Precisa de ajuda? Dê um “alô” no Mastodon ou me mande um e-mail.