Artefatos e Dados
Vamos entender um pouco sobre como trabalhar com artefatos e saída dos jobs e cache entre as dependências.
Quando se trata de trabalhar com dados em um workflow há uma ampla variedade de definições de dados que podemos nos referir.
Vamos imaginar que um job faz o build de um app. Esse build poderia produzir os arquivos de um site que serão carregados em um webserver, os executaveis para criar um container, um instalador para uma aplicação desktop, um app mobile para lojas de apps, etc. Esses arquivos são chamados de artefatos que são os outputs (ativos) gerado por um job.
No github podemos fazer o download e salvar os artefatos produzidos por um job manualmente ou usá-los em outro job subsequente para fazer o deploy, montar uma imagem, ou qualquer outra coisa.
Vamos para o cenário mais simples, um website.
name: Deploy website
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Lint code
run: npm run lint
- name: Test code
run: npm run test
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Build website
run: npm run build # ESte comando gera uma pasta dist que contém o que precisamos para o deploy
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy
run: echo "Deploying..." #Nesse momento seria necessário ter os arquivos em mão, mas se não salvamos anteriormente no processo de build perdemos tudo assim que o job termina.
Executando localmente o projeto temos.
Para ter acesso aos arquivos o que poderíamos fazer?
...
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Build website
run: npm run build
- name: Upload artifacts
uses: actions/upload-artifact@v4 # Vamos usar uma action para isso
# A action possui algumas configurações
with:
name: dist-files
# Todos os paths que queremos carregar ou não carregar no caso do ! na frente.
path: |
dist
!dist/**/*.md
!dist/**/*.tmp
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy
run: echo "Deploying..."
Usando esse workflow temos o nosso cenário inicial.
Podemos ver o arquivo disponível para download manualmente.
Agora vamos usá-lo no job seguinte para fazer o deploy.
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Get Build artifacts
uses: actions/download-artifact@v4 # Usamos essa action
with:
name: dist-files # Mesmo nome que usarmos para fazer o upload
# Só por curiosidade vamos listar o diretório que estamos e o anterior
- name: list current dir
run: ls
- name: list parent dir
run: ls ..
- name: Deploy
run: echo "Deploying..."
Podemos observar que o action de download faz o download do zip e descompacta na mesma pasta que estamos e remove o dist-files.zip.
Quando listei o que tinhamos na pasta foi para conferir isso.
Jobs Outpus
Além dos artefatos que são pastas e arquivos temos o Job Outputs. São valores mais simples mais que importam para serem usados em jobs subsequentes como por exemplo o nome de um arquivo, hashes, valores randômicos, etc.
Jobs outputs serão mais estudados mais adiante quando fizermos uma action personalizada.
Se olhar nas imagens acima verá que o comando npm run build
produz uma pasta dist/assets onde temos um arquivo com o nome index.xxxxxxxx.js sendo xxxxxxxx é um número randômico. Vamos imaginar que precisamos desse nome no próximo job de deploy.
name: Deploy website
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Lint code
run: npm run lint
- name: Test code
run: npm run test
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Build website
run: npm run build
- name: Output JS Filename
# Este é o identificador do step que estamos criando para usar depois no output
id: output-js-filename
# procurando um arquivo do tipo file finalizando em .js e salvando em uma variavel js_filename. Depois estamos criando um chave valor dizendo que js-filename é igual ao conteúdo de js_filename que é o nome do arquivo.
# estamos colocando tb a chave-valor dentor de GITHUB_OUTPUT
run: |
js_filename=$(find dist/assets -type f -name "*.js" | awk -F/ '{print $NF}')
echo "js-filename=$js_filename" >> $GITHUB_OUTPUT
- name: Upload artifacts
uses: actions/upload-artifact@v4 # Vamos usar uma action para isso
# A action possui algumas configurações
with:
name: dist-files
# Os paths que queremos carregar ou não carregar no caso do ! na frente.
path: |
dist
!dist/**/*.md
!dist/**/*.tmp
# Chave para definir quais são os outputs
outputs:
# Aqui, build-output é o nome do output do job, e está pegando o valor js-filename do step com o id output-js-filename.
# {{ steps }} é uma palavra reservada que faz referência a um contexto e usamos o id para encontrar o que queremos.
build-output: ${{ steps.output-js-filename.outputs.js-filename }}
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Get Build artifacts
uses: actions/download-artifact@v4
with:
name: dist-files
- name: Print JS Filename
# usar a palavra especial needs consegue referenciar os jobs.
run: echo "${{ needs.build.outputs.build-output}}"
- name: Deploy
run: echo "Deploying..."
Rodando esse workflow.
Parece complicado, mas não é muito não, vamos ter muito estudo disso mais pra frente.
Cache
Se obsevar bem verá que cada workflow levou cerca de 1 minuto para executar pois é um workflow simples, mas poderiam ser bem complexo levando muito mais tempo.
O fato de separar o que precisamos fazer por jobs torna ainda o processo mais demorado uma vez que todo o ambiente precisa ser criado para rodar cada um dos jobs e ainda a espera sequencial que montamos não rodando nada em paralelo por questões de dependências.
Nesse caso o workflow demorou a soma de todos os jobs mais o tempos que leva para montar cada um dos runners.
Claro que jobs sem dependências que rodam em paralelo melhoram a performance mas nesse caso não seria uma opção.
Podemos observar que temos jobs que executam os mesmo steps aqui.
name: Deploy website
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
## BLOCO DE STEPS IDÊNTICOS ##
- name: Get code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
##############################
- name: Lint code
run: npm run lint
- name: Test code
run: npm run test
build:
needs: test
runs-on: ubuntu-latest
steps:
## BLOCO DE STEPS IDÊNTICOS ##
- name: Get code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
##############################
- name: Build website
run: npm run build
- name: Output JS Filename
id: output-js-filename
run: |
js_filename=$(find dist/assets -type f -name "*.js" | awk -F/ '{print $NF}')
echo "js-filename=$js_filename" >> $GITHUB_OUTPUT
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist-files
path: |
dist
!dist/**/*.md
!dist/**/*.tmp
outputs:
build-output: ${{ steps.output-js-filename.outputs.js-filename }}
deploy:
...
Get code com o actions checkout não leva muito tempo, mas instalar as depedências sim e este é um passo muito comum na grande maioria dos códigos que trabalhamos.
Além de melhorar a velocidade do trabalho e da equipe, reduzir o tempo reduz o custos se você estiver pagando ou se estiver no plano free salva o tempo grátis.
A idéia criar cache para ser aproveitado em outro job ao invés de executar outra vez. Esse cenário é importante em casos que os arquivos não mudam com frequência, pois isso que fazer no checkout não faz sentido, pois mudam constantemente e o tempo é pequeno.
Podemos criar o cache entre workflows não só entre os jobs de um mesmo workflow. Um step de uma execução de um workflow pode pegar o cache de outro workflow no mesmo step.
Existe uma action só pra isso chamada cache e vamos usá-la agora.
Antes vamos entender sobre o que vamos fazer. Toda vez que alteramos um arquivo o hash dele muda e se o arquivo não for alterado o hash é o mesmo. Para sabermos se podemos aproveitar o cache precisamos ter certeza de que ele não foi alterado.
O arquivo package-lock.json no Node.js é um arquivo gerado automaticamente pelo gerenciador de pacotes npm (Node Package Manager) quando você executa comandos como npm install
ou npm ci
. Ele é criado para garantir que as instalações de dependências sejam reproduzíveis e consistentes. Se o hash desse arquivo alterar sabemos que o cache não pode ser reaproveitado.
Vamos usar a função hashFile para gerar o hash.
name: Deploy website
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
# Precisa ser antes do que queremos criar o cache na primeira execução do código, por isso aqui em test.
# Essa action avisa ao github para guardar ou sincronizar o path que definimos em algum lugar no github cloud
- name: Cache dependences
uses: actions/cache@v4
with:
# No caso do node quando instalamos as dependências ela irá para esta pasta no home o usuário.
path: ~/.npm
# Se a chave existir irá restaurar senão irá gerar
key: deps-node-modules-{{ hashFiles('**/package-lock.json') }}
# Esse passo será executado sempre, mas com as depedências já instaladas será muito mais rápido.
- name: Install dependencies
run: npm ci
- name: Lint code
run: npm run lint
- name: Test code
run: npm run test
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
## Copiamos esta parte ##
- name: Cache dependences
uses: actions/cache@v4
with:
path: ~/.npm
key: deps-node-modules-{{ hashFiles('**/package-lock.json') }}
#########################
# Poderíamos retirar esta parte do código? Sim, faz mal deixar? Não.. Então deixa que garante que se algo der errado ainda executará.
- name: Install dependencies
run: npm ci
- name: Build website
run: npm run build
- name: Output JS Filename
id: output-js-filename
run: |
js_filename=$(find dist/assets -type f -name "*.js" | awk -F/ '{print $NF}')
echo "js-filename=$js_filename" >> $GITHUB_OUTPUT
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist-files
path: |
dist
!dist/**/*.md
!dist/**/*.tmp
outputs:
build-output: ${{ steps.output-js-filename.outputs.js-filename }}
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Get Build artifacts
uses: actions/download-artifact@v4
with:
name: dist-files
- name: Print JS Filename
# usar a palavra especial needs consegue referenciar os jobs.
run: echo "${{ needs.build.outputs.build-output}}"
- name: Deploy
run: echo "Deploying..."
Vemos que na hora do teste ele não encontrou o key.
Porém no build ele encontrou pois o test acabou criando o cache.
Se fizermos uma alteração no código e não mexer nas bibliotecas no próximo workflow usará o cache mesmo em diferentes execuções de workflow. Vamos comparar as execuções.
Se atualizarmos as bibliotecas localmente usando o npm update
veremos que o arquivo package-lock.json irá mudar inclusive o git irá subir a diferença o hash irá mudar e o test da proxima execução não terá o key não recuperando cache e gerando um novo.
❯ npm update
npm warn deprecated @humanwhocodes/[email protected]: Use @eslint/config-array instead
npm warn deprecated @humanwhocodes/[email protected]: Use @eslint/object-schema instead
added 66 packages, removed 29 packages, changed 175 packages, and audited 413 packages in 26s
113 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
❯ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: package-lock.json
no changes added to commit (use "git add" and/or "git commit -a")
❯ git add package-lock.json
❯ git commit -m "update packages"
❯ git push origin main