Um workflow que executa npm ci a cada push baixa do registro as mesmas centenas de pacotes todas as vezes, mesmo quando o lockfile não muda há uma semana. Num projeto com uma árvore de dependências real, isso é um ou dois minutos perdidos antes mesmo do primeiro teste começar, e você paga esse custo de novo a cada push e a cada pull request. O actions/cache guarda esses downloads entre execuções e os devolve quando nada de relevante mudou.

Como a action decide entre hit e miss

O actions/cache armazena cada entrada sob uma string que você escolhe, a key, com escopo para o branch que a criou. Uma execução pode ler caches do próprio branch e do branch padrão. Ao restaurar, a action compara sua key com o que está salvo:

  • Uma correspondência exata com key é um hit. Os arquivos são baixados e o job continua.
  • Sem correspondência exata, cai para restore-keys, verificadas em ordem, cada uma uma correspondência de prefixo contra as keys existentes. Vence a correspondência criada mais recentemente.
  • Sem nenhuma correspondência, o passo não restaura nada. O job começa frio e, se for bem-sucedido, salva uma nova entrada sob key.

É por isso que o padrão comum inclui o hash do lockfile na key:

1
2
3
4
5
6
- uses: actions/cache@v6
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-npm-

Mude uma linha em package-lock.json e o hash muda, então a key exata falha de propósito. Você quer um cache que corresponda ao novo lockfile, não ao da semana passada. O prefixo de restore-keys ainda corresponde, então o job começa a partir do cache anterior em vez de um diretório vazio, baixa só o que mudou, e salva uma nova entrada de correspondência exata quando é bem-sucedido.

Valem dois limites. Um repositório tem 10 GB de armazenamento de cache, compartilhados entre todos os seus workflows, e qualquer entrada sem uso por 7 dias é removida. Você não precisa limpar nada manualmente: quando o repositório ultrapassa o limite, as entradas mais antigas são descartadas, e a próxima execução que precisava delas paga uma única vez o custo da instalação a frio.

Fazendo cache do npm com a opção cache do setup-node

Para os gerenciadores de pacotes mais comuns, você nem precisa escrever um passo actions/cache. actions/setup-node, actions/setup-python e actions/setup-java aceitam um input cache que cuida da key, da restauração e do salvamento por você:

1
2
3
4
5
6
7
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
  with:
    node-version: 24
    cache: 'npm'
- run: npm ci
- run: npm test

cache: 'npm' encontra package-lock.json (ou npm-shrinkwrap.json) na raiz do repositório, calcula o hash e coloca em cache os downloads do npm. cache: 'yarn' faz o mesmo com yarn.lock. Num monorepo em que o lockfile fica em packages/api/package-lock.json, aponte para ele explicitamente:

1
2
3
4
5
- uses: actions/setup-node@v7
  with:
    node-version: 24
    cache: 'npm'
    cache-dependency-path: packages/api/package-lock.json

setup-python funciona da mesma forma: cache: 'pip' (ou 'pipenv', 'poetry'), com hash calculado por padrão sobre requirements.txt. setup-java aceita cache: 'maven' ou cache: 'gradle'. Comece por aqui. Só passe para um passo actions/cache manual quando precisar de uma key ou path que a setup action não oferece, ou quando estiver fazendo cache de algo que não tem nada a ver com um runtime de linguagem.

Fazendo cache de Maven e pip direto com actions/cache

Um passo manual sempre tem as mesmas três partes: o diretório em que a ferramenta escreve o próprio cache, uma key construída a partir do arquivo que fixa as versões das dependências, e um fallback de prefixo.

1
2
3
4
5
6
- uses: actions/cache@v6
  with:
    path: ~/.m2/repository
    key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
    restore-keys: |
      ${{ runner.os }}-m2-
1
2
3
4
5
6
- uses: actions/cache@v6
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
    restore-keys: |
      ${{ runner.os }}-pip-

path é o cache da ferramenta, não do seu projeto: o cache de downloads do pip, não seu virtualenv; o repositório local do Maven, não target/. Você também pode fazer cache da saída do build, como target/ ou dist/, mas isso só compensa se o build for incremental. Do contrário, você está comprimindo e enviando arquivos que a próxima execução vai descartar.

Se você já faz cache de camadas Docker com cache-to: type=gha, como em Construir e Publicar uma Imagem Docker com GitHub Actions, essas camadas vão para o mesmo serviço de cache do Actions. Elas contam contra os mesmos 10 GB e a mesma remoção de 7 dias que seus caches de dependências. Um repositório que faz as duas coisas atinge o limite mais cedo do que você imagina, e caches de camadas grandes podem expulsar seu cache do npm.

Keys de cache em builds matrix

Uma matrix com várias versões do Node ou sistemas operacionais precisa do valor da matrix dentro da key, ou os três legs vão disputar uma única entrada:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
strategy:
  matrix:
    node-version: [20, 22, 24]
steps:
  - uses: actions/cache@v6
    with:
      path: ~/.npm
      key: ${{ runner.os }}-npm-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }}
      restore-keys: |
        ${{ runner.os }}-npm-${{ matrix.node-version }}-

Entradas de cache não podem ser sobrescritas. Remova matrix.node-version da key e os três legs vão tentar salvar Linux-npm-<hash>: vence o primeiro que terminar, e os outros registram um warning de que o salvamento falhou. Para ~/.npm isso é quase inofensivo, já que os tarballs baixados não dependem da versão do Node. Deixa de ser inofensivo quando o path em cache contém saída compilada, como node_modules com um addon nativo. Aí o leg do Node 24 restaura binários compilados para o Node 20, e você tem falhas que parecem qualquer coisa menos um problema de cache. runner.os na key faz o mesmo trabalho entre legs de sistemas operacionais diferentes.

O que não colocar em cache no GitHub Actions

Trate uma entrada de cache como uma camada de imagem: qualquer um que possa executar um workflow que a restaure pode lê-la. Mantenha tokens e arquivos .env fora de qualquer diretório em cache. A regra é a mesma de Variáveis de Ambiente e Secrets no Docker: secrets vêm de secrets: em tempo de execução, nunca de algo escrito em disco e reutilizado.

Dois casos em que fazer cache custa mais do que economiza:

  • Qualquer coisa que se instale em poucos segundos. Um punhado de pacotes Python puros, sem extensões compiladas, se instala quase tão rápido quanto se restaura. O passo de cache tem seu próprio overhead para comprimir e transferir o arquivo, e isso pode anular uma economia tão pequena.
  • Uma key abrangente demais para algum dia dar hit. Faça o hash de todo o diretório src/ em vez do lockfile e cada mudança de código invalida o cache. Baseie a key no arquivo que fixa as versões das dependências e em nada mais.

Como saber se o cache está dando hit

O passo actions/cache registra Cache restored from key: ... num hit e Cache not found for input keys: ... num miss. Leia essa linha antes de confiar na configuração.

Para medir, adicione cache: 'npm' ou o passo manual a um workflow, faça push duas vezes e compare quanto tempo o passo npm ci leva na execução fria e na quente. Essa diferença é o que a mudança vale. Se um job deve construir o cache e outros jobs devem só lê-lo, substitua actions/cache por actions/cache/restore@v6 e actions/cache/save@v6, que separam as duas metades para que você decida onde cada uma roda.

Artigos relacionados