Introdução
A ideia deste workshop é explorarmos automações para facilitar a experiência do desenvolvimento utilizando como ambiente o Github Actions.
O objetivo final é chegar com conhecimento o suficiente para criação de um sistema de CI funcional e, se tudo der certo, falaremos um pouco sobre a entrega contínua.
CI/CDs
O CI/CD é um dos pilares da cultura DevOps.
Para garantir que os times consigam entregar mais e com mais frequência, a prática do CI/CD é fundamental.
Essa é uma sigla muito utilizada nos dias de hoje e, nesta parte, iremos discutir um pouco sobre o que ela significa e suas implicações no dia a dia.
Integração contínua (Continuous Integration)
A integração contínua é uma prática onde os desenvolvedores sobem alterações constantes no repositório. A cada alteração, há um processo definido de build e teste automatizado para garantir que as alterações não quebraram nada.
Um dos maiores benefícios do CI é garantir que o repositório está sempre o mais atualizado possível e sempre com corretude.
Entrega Contínua (Continuous Delivery)
A etapa de entrega contínua é o passo seguinte ao CI. Consiste em ter automações para a entrega do novo código nos ambientes: seja de dev, tst ou prod.
A automação do processo de entrega de código para os ambientes finais, quando feito como código, serve também como uma documentação e pode ser versionada no próprio repositório. Além disso, acaba reduzindo a carga da pessoa desenvolvedora em dois níveis:
- carga cognitiva: a pessoa desenvolvedora não precisa ficar redescobrindo comandos arcanos para realizar o deploy. Idealmente o processo é iniciado com alguns cliques (ou automaticamente).
- carga processual: a máquina da pessoa desenvolvedora não fica onerada com o processamento de construção do artefato para ser implantado já que todo esse processamento ocorrerá em um servidor especifico para essa tarefa.
Até pouco tempo atrás, deploys consistiam em copiar o build local para um servidor via FTP. Olha só como avançamos!!
Diferença entre CDs?
Existe também a prática de Implantação Contínua (Continuous Deployment). Enquanto o deploy para produção é feito de forma manual na Entrega Contínua, com a Implantação Contínua deploy para produção/cliente final, é feito de forma automática e apoiado por uso de feature flags.
Github Actions
Apesar de existir uma infinitude de ferramentas para criação de sistemas de CI/CD, a que utilizaremos aqui é o Github Actions.
Alguns motivos são:
- integração com Github
- flexibilidade na criação dos sistemas
- comunidade
Workflows
Para definirmos o que é um workflow, podemos buscar a definição da própria documentação:
Um fluxo de trabalho é um processo automatizado configurável que executa um ou mais trabalhos. Os fluxos de trabalho são definidos por um arquivo YAML verificado no seu repositório e será executado quando acionado por um evento no repositório, ou eles podem ser acionados manualmente ou de acordo com um cronograma definido.
Ou seja, são automações definidas via yaml que são configuradas de acordo com cada necessidade.
Um workflow é composto por pelo menos um gatilho e pelo menos um job.
Vamos dar uma olhada num workflow simples.
name: workshop
on: [push]
jobs:
hello-world:
runs-on: ubuntu-latest
steps:
run: echo "Hello World"
Neste workflow, definimos:
name: nome do workflowon: gatilho que acionará a automaçãojobs: uma lista de um ou mais processoshello-world: o nome do nosso jobruns-on: definimos o runnersteps: as etapas que compõem o nosso job
Apesar de simples, já tem bastante coisa envolvida.
Nessa parte, vamos nos aprofundar um pouco mais em cada um desses atributos.
Executores
Mas fica a pergunta: onde vai rodar isso tudo?
Bem, existem duas opções para execução dos nossos workflow:
- provisionados pelo github
- autoprovisionado
Se reparar no exemplo que acabamos de ver, o job tem um parâmetro runs-on para indicação de qual é o executor a ser utilizado.
O que é?
De forma bem resumida, um executor (ou runner), é um ambiente efêmero cuja única função é executar um job definido no nosso workflow.
Na introdução da estrutura de um workflow, comentei que eles podem conter mais de um job e cada um deles deve ter uma definição de qual runner é necessário.
Que tal um exemplo mais prático? Imagine que você está desenvolvendo uma aplicação Desktop e precisa garantir o suporte e a compilação de 3 ambientes: Linux, Windows e Mac. Como cada job tem um executor próprio, você pode definir os 3 jobs de compilação no mesmo workflow.
github-hosted
Os runners provisionados pelo Github são os mais simples de serem utilizados.
Se você não tem muito requisito de ambiente, o mais comum é o ubuntu-latest com o Linux.
Há também runners com sistemas Windows e macOS.
Você pode encontrar nesta tabela quais são as característica de cada uma das máquinas disponíveis para repositórios públicos e nesta aqui, para os repositórios privados.
Você certamente já ouviu a expressão "não existe almoço grátis". Almoço eu não sei, mas uma coisa é certa: não existem executores grátis. Pelo menos não ilimitado! Nessa página você consegue ver quais são os limites de cada um dos runners.
self-hosted
Se na sua realidade for importante a execução do seu workflow em um ambiente específico (sua cloud ou servidores on-premises), não se desespere!
O Github permite também que você utilize a sua infra própria para subir os executores! Exige um pouco mais de configuração e não abordaremos isso aqui, mas é possível!
Assim como nos outros, também existem regras e limites de uso. A documentação está aqui, caso tenha curiosidade!
Gatilhos
Como os workflows são acionados
Os gatilhos são definidos sempre no atributo on dos workflows.
Existem diversos métodos para acionarmos os workflows, mas os principais são:
-
workflow_dispatch: acionamento manual. É possível definirmos entradas para parametrização da execução -
workflow_call: acionado via outro workflow. Também é possível definirmos entradas. -
schedule: acionado em um determinado padrão de horário. -
push: acionado sempre que acontecer um push.Se quiser ignorar algum gatilho via push/pr, basta utilizar um
[skip ci]na mensagem de um commit. -
pull_request: acionado sempre que um pull request for criado, fechado, etc. Podemos especificar coisas como:branches/branches-ignore: acionar apenas quando acontecer em uma determinada branch ou evitar a execução nela. No caso dos pull requests é verificada a branch alvo do PR.paths/paths-ignore: acionar apenas quando acontecer alteração em um caminho ou ignorar se algum arquivo for alterado. Um bom uso para ignorar caminhos é evitar deploys de arquivos que não são parte do código fonte, por exemplo, oREADME.mdde um projeto.
O uso do
paths/paths-ignoreé mutuamente exclusivo. Para ignorar um caminho ao usar opaths, basta utilizar o prefixo de negação!.A inclusão de um caminho depois de uma exclusão, adiciona o caminho novamente. Ou seja, a preferência é sempre decrescente.
Exemplos
Acionamento manual
name: Aciona aqui
on: workflow_dispatch
# ...
cronjob
name: Roda todos os dias às 07h (UTC)
on:
schedule:
- cron: '0 7 * * *'
# ...
Deploy com push na main
name: Deploy da main
on:
push:
branches:
- 'main'
# ...
Deploy apenas com alterações em código na main
name: Deploy na main com alteração de código
on:
push:
branches: [main]
paths:
- "src/**.js"
# ...
Referências
- Documentação oficial: Eventos que disparam fluxos de trabalho
- Documentação oficial: Ignorar execuções de fluxo de trabalho
Variáveis de ambiente
Assim como no ambiente de desenvolvimento, podemos definir variáveis para parametrizar o comportamento de alguns programas ou fluxos.
Um exemplo clássico é o uso da variável NODE_ENV para definir em qual ambiente o projeto será executado.
$ NODE_ENV=production npm run build
$ NODE_ENV=development npm run build
No ambiente de produção são feitas algumas otimizações que são dispensáveis no ambiente de desenvolvimento.
Escopo
Podemos definir uma variável em 3 níveis:
- workflow
- job
- etapa
Para definir uma variável, basta definirmos um mapa env. Todos os elementos abaixo daquele escopo terão acesso à variável. Por esse motivo, é bom seguirmos uma das regras de ouro da segurança: quanto menor o escopo, melhor.
Exemplos
Definir versão do NodeJS via NVM
name: Instala a versão do NodeJS
on: # ...
env:
NODEJS_VERSION: 22.3.0
jobs:
install-node:
runs-on: ubuntu-latest
steps:
# ... Instala o NVM
- name: Define versão do NodeJS
run: nvm use $NODEJS_VERSION
Saídas (outputs) e dependências
Às vezes precisamos passar informações de uma etapa para outra e até de um job para outro. Para resolver esse problema, podemos utilizar as saídas (outputs).
É importante notar que os outputs são apenas strings e ficam armazenados em um arquivo de saída do workflow.
O caminho deste arquivo está disponível através da variável GITHUB_OUTPUT e este arquivo é compartilhado por todas as etapas de um job.
Para definir uma saída de um job, basta usar o atributo outputs. Para acessar, utilizamos o steps.<nome da etapa>.outputs.<nome da saída> ou needs.<nome do job>.outputs.<nome da saída>.
name: Job com uma saída
# ...
jobs:
job1:
runs-on: ubuntu-latest
outputs:
olamundo: ${{ steps.etapa1.outputs.ola }}
steps:
- id: etapa1
run: echo 'ola=mundo' >> $GITHUB_OUTPUT
job2:
runs-on: ubuntu-latest
needs: job1
steps:
- env:
SAIDA1: ${{ needs.job1.outputs.ola }}
run: echo "Olá $SAIDA1"
Observe que na linha needs: job1 definimos que o job2 só será executado após uma execução bem sucedida do job1. Caso essa dependência não fosse definida, o job2 seria executado de forma paralela ao job1 e o comportamento do nosso workflow poderia não ser o desejado.
Perceba que na definição do output, utilizamos a keyword run para executar um comando dentro do runner. Para quem não é muito chegado no bash, utilizamos um print com o comando echo e redirecionamos a saída para o arquivo presente em $GITHUB_OUTPUT.
Alguns casos de uso
Gerar tag do projeto utilizando a versão do package.json
# ...
steps:
# ... Checkout e configuração de credenciais do git
- name: Atualiza versão
run: npm version minor
- name: Versão do projeto
id: version
run: echo version=$(npm pkg get version | sed 's/"//g') >> $GITHUB_OUTPUT
- name: Cria tag com a nova versão
env:
VERSION= ${{ steps.version.outputs.version }}
run: |
git tag -a "v$VERSION"
git push --tags
Referências
- Documentação oficial: Definindo saídas para trabalhos
Segredos e variáveis
Um dos primeiros conceitos que encontramos ao iniciar no mundo da programação é o uso de variáveis. Isso se deve ao fato de que às vezes o nosso código precisa receber alguma entrada do usuário, além de facilitar a manutenção e leitura do código.
Com os workflows não é diferente. Para isso, existem os secrets e as variables.
Além disso, o Github Actions possui uma abstração chamada environments para facilitar o controle de secrets e variables em diferentes ambientes como dev, tst e prod, por exemplo.
secrets e variables
Um secret e uma variable são parâmetros que podem ser acessados pelo workflow e a principal diferença entre eles é que o secret, como o nome dá a entender, é secreto. Ou seja, depois de definido, apenas os workflows podem acessá-los. O próprio GHA vai ocultar os valores dos secrets se eles fossem exibidos no log.
Se for um dado sensível, como uma credencial, crie um secret. Caso contrário, use uma variable.
Mas como fazemos para criar um secret? Vamos ver isso agora!
Níveis e criação dos parâmetros
Esses parâmetros podem ser definidos a nível de organização, repositório e ambientes. Se houver colisão de nomes, ou seja, um secret chamado SECRET_SECRETO_DO_JP a nível de organização e um outro chamado SECRET_SECRETO_DO_JP a nível do repositório, a preferência é sempre do secret do repositório!
Se ainda houvesse um secret chamado SECRET_SECRETO_DO_JP a nível de ambiente, a preferência seria do ambiente!
Em resumo: quanto menor o escopo, maior a preferência.
Para a criação, basta ir na configuração do nível desejado:
- repositório: aba de configurações do repositório > Segurança > Segredos e variáveis > Actions > Segredos / Variáveis > Novo segredo/variável
- ambiente: aba de configurações do repositório > Ambientes > Selecione o ambiente > Segredos / Variáveis > Novo segredo/variável
- organização: aba de configurações da organização > Segurança > Segredos e variáveis
Utilização
Os segredos e variáveis estão disponíveis através do objeto secrets e vars, respectivamente. Ou seja, para acessar o valor do secret SECRET_SECRETO_DO_JP, basta expandirmos o valor via ${{ secrets.SECRET_SECRETO_DO_JP }}.
Exemplos
Notificação de webhook
name: Notifica no slack
on: # ...
jobs:
send-notification:
runs-on: ubuntu-latest
steps:
- uses: slackapi/slack-github-action@v1.26.0
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Credenciais da AWS
name: Autentica credencial da AWS
on: # ...
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4.0.2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ${{ vars.AWS_DEFAULT_REGION }}
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
role-duration-seconds: 3600
Etapas / Steps
Nos capítulos anteriores vimos alguns exemplos de workflows, mas passamos batido por um detalhe importante: apesar de sabermos que um job é composto por etapas, o que compõe uma etapa?
Podemos definir dois tipos de etapas:
- comandos
- ações (actions)
Comandos
Podemos indicar a execução de um comando através da palavra-chave run. Como vimos em alguns exemplos anteriores, ao utilizar o run, interagimos diretamente com uma shell e podemos inclusive fazer uma cadeia de comandos.
# ...
steps:
- name: Dá oi
run: echo "Olá mundo!"
- name: Pergunta se está tudo bem e se despede
run: |
echo "Tudo bem?"
echo "Tchau!"
Parâmetros
A melhor maneira de passarmos parâmetros para os comandos é através das envs.
# ...
steps:
- name: Imprime BAR
env:
FOO = "BAR"
run: echo "$FOO"
Actions
Para tarefas mais complexas, podemos utilizar actions. Elas são componentes que permitem a repetição de atividades e reduzem a complexidade do seu workflow.
Para invocarmos uma action, utilizamos a palavra-chave uses.
Versão
Imagina só esse cenário: na semana passada o seu time gerou uma release utilizando o pipeline. Quando chegou a sua vez de tirar a release, você se deparou com um erro. O mantenedor de uma das actions que você utiliza aproveitou pra trabalhar nela no tempo livre do fim de semana e acabou subindo uma alteração que quebrou a compatibilidade com o seu workflow.
COMO ASSIM????
É. Isso pode acontecer. Esse workshop é pra mostrar como o Github Actions é legal e útil, não que sua relação com ele vai ser perfeita e mil maravilhas.
Mas calma, tem uma maneira da gente se proteger desse tipo de problema. E não é uma surpresa pra ninguém que tenha passado pelo Inferno de dependências.
E se a gente especificar exatamente qual a versão da action estamos interessado no nosso workflow? Para fazer isso, basta passarmos uma referência via @ depois do nome da action. Essa referência pode ser uma tag ou, caso o criador da action não tenha sua confiança, a chave SHA do commit para garantir mais segurança.
# ...
uses: uma/actionmuitomaneira@<SHA/tag>
Parâmetros
Para passarmos parâmetros para as actions, geralmente utilizamos a palavra-chave with.
# ...
steps:
- name: Clona o repositório e vai pra branch principal
uses: actions/checkout@v4
with:
ref: main
Actions úteis
Abaixo destaco algumas actions que acho interessantes.
actions/checkout: permite clonar o repositório e fazer ocheckoutem uma revisão específica.actions/setup-node: instala a versão especificada donode.googleapis/release-please-action: Criação automática de releases utilizando Conventional Commits.aws-actions/configure-aws-credentials: configura uma credencial da AWS para utilização de serviços comoaws-clie outros.SonarSource/sonarcloud-github-action: integração com o SonarQube.softprops/action-gh-release: criação automática de release.actions/cache: permite o cache de artefatos para redução do tempo de execução do workflowactions/upload-artifact: realiza upload de arquivos comoCHANGELOG.mdou arquivos de cobertura de testes do código.actions/install-nix: instalanixno runner.actions/docker-setup-buildx: instaladockerno runner.
Além dessas, há uma infinitude de actions que você pode buscar no Github Marketplace - Actions e filtrar por categorias.
Actions extremamente específicas pra caramba
Caso você tenha buscado e não encontrou uma action que resolva o seu problema, não se desespere! Assim como você não encontrou aquela pizzaria que faria aquela pizza de nutella e abacaxi ou outras combinações altamente questionáveis, é sempre possível partirmos para uma solução caseira.
Existem 3 tipos de actions a serem criadas:
- container Docker
- JavaScript
- ações compostas
Não vamos abordar isso no workshop, mas você sempre pode contar com a documentação oficial para aprofundar nos tópicos discutidos aqui!
Mão na massa
Beleza. Tudo lindo, tudo bacana. Mas vamos pra prática?
Requisitos
- Conta no Github: afinal de contas é a plataforma que estamos falando desde o início
- Criação de repositório
- Criação de um secret para armazenar a credencial do NPM
- Criação de um Personal Access Token (PAT) para utilização no workflow
- Conta no NPM: onde publicaremos nossa biblioteca
- Criação de uma credencial para publicação da biblioteca
Pessoalmente gosto bastante de CLIs e recomendo também a
gh-cli. É opcional, mas facilita bastante em algumas etapas.
Agenda
Nessa etapa do workshop, vamos criar workflows para resolver dois pontos importantíssimos da etapa de desenvolvimento:
- análise de PRs
- criação de release
Na etapa de análise de PR, estamos interessados em verificar se os testes estão passando. Você pode (e deve) ser mais criterioso do que o nosso exemplo. Lembre-se que contexto é tudo!
Já na criação da release, vamos assumir um mundo perfeito: toda alteração que chega na branch é uma release nova, afinal tudo passou pelo crivo das nossas análises dos PRs.
Vamos utilizar como base um projeto em node, mas você é livre para escolher a linguagem/ecossistema que preferir.
Lembre de atualizar as actions para refletir a sua escolha.
Criando um projeto
Essa etapa é a mais tranquila. É tipo aquela prova do colégio que o professor dava meio ponto só por preencher o cabeçalho.
Criação do repositório
Vamos criar um repositório vazio. O nosso projeto vai ser uma biblioteca responsável por (pasme!!!) dizer um 'Olá' para o mundo!!
Com a
gh-cli, você pode criar o repositório com o comando$ gh repo create jpedrodelacerda/workshop-gha --cloneEle também clona o repositório pra você.
Criação do projeto
Depois do repositório clonado, vamos iniciar o projeto usando o npm init.
Você pode utilizar também
pnpmouyarnse preferir, mas lembre de alterar as actions.
$ cd workshop-gha
$ npm init -y
Além de criar o projeto, é sempre bom ter um arquivo README.mdno projeto.
$ echo "# Workshop GHA" >> README.md
Pronto. Temos nosso repositório iniciado.
Desenvolvedores responsáveis
Como bons desenvolvedores que somos, não podemos esquecer dos testes!
Para isso, utilizaremos o jest.
$ npm install --save-dev jest
Lembre de commitar e enviar
$ git add README.md package.json package-lock.json
$ git commit -m "chore: initial commit"
$ git push
Afinal, de que adianta se as alterações ficam só na nossa máquina?
Automatizando etapas do PR
Nessa etapa, o objetivo é desenvolver um workflow que será executado para automatizar os testes de um PR. Dessa forma, conseguimos reduzir o ruído na hora da revisão do código. Afinal, não faz muito sentido revisar um PR em que os testes estão quebrados.
Criação do primeiro workflow
Para criar um workflow, precisamos de um arquivo em .github/workflows/.
O primeiro passo vai ser definir quais são os gatilhos para o nosso workflow.
Queremos que a checagem sempre seja executada quando um PR é criado.
Podemos especificar, por exemplo, que só execute os testes nos PRs que tenham como alvo uma branch com determinado nome combinando
pull_requesto atributo ebranches. Mas esse não é o nosso caso!
name: PR Checker
on:
pull_request:
Isso é suficiente? Sim, mas podemos ser ainda mais específicos.
Faz sentido executar testes em PRs que alteram apenas arquivos de documentação? Se eu fiz uma alteração no README.md, tem motivo executarmos testes. Especificamos isso utilizando paths.
name: PR Checker
on:
pull_request:
paths:
- '**.js'
Checkout
O próximo passo é garantir que o runner tem acesso ao repositório.
name: PR Checker
on:
pull_request:
paths:
- '**.js'
jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
Configuração do Node
Agora que temos acesso ao repositório, precisamos garantir que temos o node instalado.
name: PR Checker
on:
pull_request:
paths:
- '**.js'
jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
Execução dos testes
Para execução dos testes, precisamos instalar as dependências e executar o script.
name: PR Checker
on:
pull_request:
paths:
- '**.js'
jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- name: Prepare | Checkout repo
uses: actions/checkout@v4
- name: Prepare | Setup node
uses: actions/setup-node@v4
with:
node-version: 22.x
- name: Prepare | Install dependencies
run: npm i
- name: Run tests
run: npm run test
Cache
O workflow está pronto e funcional! Porém, podemos fazer uma otimização boa nele. Ao invés de baixar todas as dependências sempre que executar, que tal criarmos um cache das dependências e, caso nenhuma delas tenha sido atualizadas, utilizar o que deixamos nele?
Para isso, utilizaremos a actions/cache.
name: PR Checker
on:
pull_request:
paths:
- '**.js'
jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- name: Prepare | Checkout repo
uses: actions/checkout@v4
- name: Prepare | Setup node
uses: actions/setup-node@v4
with:
node-version: 22.x
- name: Prepare | Get npm cache directory
id: npm-cache
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
- name: Prepare | Cache dependencies
id: cache
uses: actions/cache@v4
with:
path: ${{ steps.npm-cache.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Prepare | Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm i
- name: Run tests
run: npm run test
Adicionamos a actions/cache definindo os seguintes parâmetros:
path: diretório que deve ser armazenado no cachekey: um identificador para o cache. Neste caso utilizamos uma concatenação do sistema e uma hash dopackage-lock.json. Toda vez que houver uma alteração no sistema utilizado ou no arquivo de dependências, ele utilizará outro cache.A expressão
hashFilese outras estão documentadas aqui.restore-keys: conjunto de padrões do cache. Caso não tenha um match exato, ele vai tentar restaurar o cache mais próximo.Importante: a instalação de dependências só deixa de acontecer se houver um match exato da chave!
Se não encontrar, ainda vai executar a instalação das dependências. Mas ainda assim, espera-se que seja num tempo menor, pois atualizaria apenas o necessário.
Perceba também que adicionamos uma condicional na etapa de instalação de dependências: só execute a instalação das dependências caso o cache não tenha encontrado uma
Múltiplas versões em diferentes arquiteturas
Você já reparou que em alguns projetos estão disponíveis várias versões de um executável? Você precisa decidir qual plataforma vai usar, qual sistema, etc.
Imagina ter que repetir o tamanho do workflow pra dar suporte pra isso!!
Mas calma, será que realmente precisamos fazer essa duplicação? Existe alguma solução para permitir suporte para todas as combinações de forma simples?
Se nossa biblioteca tivesse suporte para outras versões do Node e outros sistemas, poderíamos executar a nossa ação em cada uma das versões através das matrizes.
name: PR Checker
on:
pull_request:
paths:
- '**.js'
jobs:
run-tests:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node: [18, 20, 22]
runs-on: ${{ matrix.os }}
steps:
- name: Prepare | Checkout repo
uses: actions/checkout@v4
- name: Prepare | Setup node ${{ matrix.node }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}.x
- name: Prepare | Get npm cache directory
id: npm-cache
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
- name: Prepare | Cache dependencies
id: cache
uses: actions/cache@v4
with:
path: ${{ steps.npm-cache.outputs.dir }}
key: ${{ runner.os }}-node-${{ matrix.node }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node }}
- name: Prepare | Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm i
- name: Run tests
run: npm run test
Atenção: caso você precise dar suporte a mais de uma arquitetura de processadores, é preciso também se atentar para o cache, sendo necessário mudar a chave para garantir compatibilidade do sistema.
Assim como na etapa anterior, se houver uma etapa específica de uma plataforma ou versão, você pode utilizar uma condicional para ignorá-la ou não.
Finalizando
Agora vamos para a publicação!!
Publicando uma release
Como estamos trabalhando com uma biblioteca, a publicação da release vai ser no npm. Para isso, basta ter uma conta registrada no site.
Queremos também gerar um texto contendo as mudanças que ocorreram desde a última release.
Nesta etapa não nos preocuparemos com teste. Assumimos no nosso mundo ideal lindo e maravilhoso que toda alteração já passou previamente pelo teste na etapa de PR. Caso seja do seu interesse, você pode definir os jobs que utilizamos na etapa anterior para executar testes antes da publicação.
Começando pelo começo
Vamos definir um nome e o gatilho que queremos para o nosso workflow no arquivo ./github/workflows/release.yaml.
Definimos anteriormente que queremos publicar uma nova versão sempre que chegar alguma alteração na branch principal. De forma muito parecida com o gatilho do pr-checker, também só estaremos interessados se essas alterações aconteceram no código fonte do projeto ou arquivos que podem indicar a criação de uma release nova.
name: Create release and publish
on:
push:
branches:
- main
paths:
- '**.js'
- 'package.json'
- 'CHANGELOG.md'
Pode ser interessante utilizar um gatilho manual na etapa de validação do seu workflow com um gatilho
workflow_dispatch, mas lembre-se de retirá-lo quando o workflow estiver funcionando normalmente.
Processo da release
Utilizaremos o conventional commit para padronização das mensagens de commit.
Se quiser dar uma lida com calma, a documentação está aqui.
Em resumo, nossas mensagens terão o seguinte formato:
<tipo>[escopo opcional]: <descrição>
Caso tenha uma quebra de compatibilidade, podemos indicar isso para o sistema utilizando um ponto de exclamação: <tipo>!: <descrição>. Isso fará com que a major version seja atualizada.
Escolhemos o conventional commits pois utilizaremos a action googleapis/release-please.
Eu falei que tudo que chegasse na main seria uma release, né? Mas não vai ser chegou e publicou. O processo vai ser com as seguintes etapas:
- criação de PR apontando para
mainDesenvolveremos nossa feature em uma branch de vida curta. Assim que finalizarmos o desenvolvimento dela, abriremos um PR para a branch principal. - merge na
mainAcionará o workflow da release. - a action irá criar um novo PR da release Com a execução do workflow da release, um novo PR é criado. Até então nossa release não foi publicada.
- merge do PR da release Com esse merge, a action criará uma release do Github e executará o resto do nosso workflow, publicando o pacote no NPM.
Preparando a publicação
Antes de começar a fazer nosso workflow de release, vamos preparar o terreno!
Por padrão, o npm publica no repositório público. Caso você deseje fazer o deploy para outro repositório, pode configurar isso via publishConfig no package.json.
Vamos aproveitar e atualizar algumas informações:
name: vamos adicionar um escopoauthor: informações pessoais.license: eu vou utilizar a MIT ou Apache-2.0. Sinta-se livre para escolher a que preferir.publishConfig.access: para definir que o pacote é publico. Pacotes privados no NPM requerem pagamento. Caso não queira configurar isso nopackage.json, você pode passar a flag--access publicno comando de publicação do workflow.
{
"name": "@jpedrodelacerda/workshop-gha-n7ff5",
"author": "João Lacerda <dev@jpedrodelacerda.com> (https://ddb.jpedrodelacerda.com/)",
"license": "(MIT OR Apache-2.0)",
"publishConfig": {
"access": "public"
}
...
}
Autenticação
Mesmo com um escopo, o que impede alguém de publicar uma nova versão do meu pacote? Ou melhor, como o NPM consegue confiar que, ao publicar uma versão, sou eu mesmo?
Para fazer a autenticação, precisamos de algum identificador gerado pelo NPM. Esse identificador é secreto e deve ser utilizado com muito cuidado.
Se você pensou em armazenar isso em um secret do repositório, pensou certo!
Depois de logar na conta do NPM, clique no seu perfil e procure por tokens de acesso. Vamos gerar um novo token de acesso granular.
Coloquei o nome no meu de workshop-gha. (Que original, não?) O tempo de vida do token fica a seu critério. Quanto menor, melhor. A não ser que vá utilizar para outros projetos.
Não se esqueça de dar permissão de leitura e escrita (read and write).
Por desencargo de consciência, vou definir também o escopo apenas para o @jpedrodelacerda.
Depois de criado, vá nas configurações do seu projeto no Github e adicione o secret NPM_TOKEN com o token criado.
Se estiver com a
gh-cli, você pode criar o secret do repositório com o seguinte comando:$ gh secret set NPM_TOKEN
Criando uma release
Para adicionar a release, precisamos garantir algumas permissões para o workflow. Vamos aproveitar e adicionar a action também.
Ações utilizando o token padrão do workflow não geram novos acionamentos para evitar loops indesejados. Portanto, vamos criar um Personal Access Token para que tanto o PR quanto o commit da release possam acionar nosso workflow.
Se tiver logado com a
gh-clie quiser reutilizar o token que está configurado para ela, você pode executar o seguinte comando:$ gh secret set PAT_TOKEN -b $(gh auth token)
Para mais informações sobre as permissões, veja a documentação da action.
name: Create release and publish
on:
push:
branches:
- main
paths:
- '**.js'
- 'CHANGELOG.md'
- 'package.json'
jobs:
release-please:
permissions:
contents: write # Permitir a criação do commit de release
pull-requests: write # Permitir a criação do PR de release
runs-on: ubuntu-latest
steps:
- name: Prepare | Create github release
uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.PAT_TOKEN }}
release-type: node
Publicação da biblioteca
Para publicar um pacote no NPM, é necessário ter uma conta registrada e precisamos também atualizar o package.json.
name: Create release and publish
on:
push:
branches:
- main
paths:
- '**.js'
- 'CHANGELOG.md'
- 'package.json'
jobs:
release-please:
permissions:
contents: write # Permitir a criação do commit de release
pull-requests: write # Permitir a criação do PR de release
runs-on: ubuntu-latest
steps:
- name: Prepare | Create github release
uses: googleapis/release-please-action@v4
id: release
with:
release-type: node
- name: Prepare | Checkout repo
if: ${{ steps.release.outputs.release_created }}
uses: actions/checkout@v4
- name: Prepare | Configure Node+NPM
if: ${{ steps.release.outputs.release_created }}
uses: actions/setup-node@v4
with:
version: 22.x
registry-url: 'https://registry.npmjs.org'
- name: Prepare | Install deps
if: ${{ steps.release.outputs.release_created }}
run: npm ci
- name: Publish | Publish package to NPM
run: npm publish
if: ${{ steps.release.outputs.releases_created }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Perceba que só publicamos se nossa release foi criada.
Não esqueça de commitar nossas alterações e vamos validar nosso trabalho!!
Validando nosso CI
Com tudo pronto, vamos criar nossa primeira release?
Primeira saudação
Antes de tudo, vamos criar uma branch nova pra essa nossa feature?
git branch -b feat/add-hello
Precisamos adicionar um código para nossa biblioteca.
-
index.js:const { hello } = require("./src/hello"); console.log(hello()); module.exports = { hello }; -
src/hello.js:const hello = () => { return "Olá mundo!"; }; module.exports = { hello };
Vamos subir nossa modificação e criar nosso primeiro PR.
$ git add index.js src/hello.js
$ git commit -m 'feat: add greeting function'
$ git push --set-upstream origin feat/add-hello
Os testes falharam. Essas falhas de pipeline incomodam, né? Pois bem, vamos corrigir isso tudo!
Testes
Agora com o teste, vamos atualizar o script de testes no package.json:
{
...
"scripts": {
"test": "jest"
}
...
}
Subindo a alteração!!
$ git add package.json
$ git commit -m 'chore: use `jest` to run tests'
$ git push
Ainda está quebrado. Mas temos progresso! O erro que está dando agora é simples de resolver!
Vamos adicionar um teste em src/hello.test.js para deixar o jest feliz!
const { hello } = require("./hello");
test("Retorna 'Olá mundo!'", () => {
expect(hello()).toBe("Olá mundo!");
});
$ git add src/hello.test.js
$ git commit -m 'test: add `hello.test.js`'
$ git push
Publicando
Bem, agora que nosso PR está testado e aprovado, vamos mergear!
Para mergear, vamos utilizar o método do squash and merge, recomendado pelo googleapis/release-please.
Pronto! Agora que nosso workflow rodou, foi criado um novo PR de release. É só mergearmos e correr pro abraço!
Se tudo der certo, você acaba de publicar um novo pacote no NPM! Parabéns!!
Próximos passos
Apesar de termos completado a publicação de nossa valiosíssima biblioteca, esse não precisa ser o final da sua jornada!
O Github Actions tem uma infinidade tanto de aplicações como suporte. Abaixo vou mostrar (bem superficialmente) algumas.
Testcontainers
Você precisa fazer testes de integração com um serviço da AWS? Quer testar sua conexão com o banco? Nada tema!!
A ideia do Testcontainers é facilitar o trabalho de subir containers descartáveis para seus testes.
O projeto possui vários módulos pra deixar ainda mais tranquila a configuração. Procurou por um módulo que não existe? Não tem problema! Você pode especificar a imagem que precisa.
Quer um ecossistema mais completo e AWS é algo importante pra ti? Talvez as imagens do localstack possam te ajudar.
Teste local
Tá com vergonha de subir uma alteração e quebrar alguma coisa? Não precisa disso, mas já que tá rolando, que tal rodar seu workflow local?
Gostou da ideia?
É pra isso que o act tenta resolver. Com suporte a diversos eventos pra simular, ele vai subir containers para que você consiga validar localmente seu workflow!