Enquanto eu configurava um ambiente de staging em uma nova plataforma de hospedagem, eu me deparei com um problema onde arquivos estáticos eram agressivamente mantidos em cache sem uma forma direta de invalidá-los. Isso tornava os deploys de staging pouco confiáveis e validar mudanças demorado.
Eu poderia ter passado horas brigando com cache headers e purge APIs, mas existe uma abordagem mais simples. Ao invés de brigar com o cache, eu abracei padrões que evitam invalidação completamente e tornam o comportamento do staging explícito e previsível.
O problema
Esse ambiente de staging estava se comportando mal:
- Arquivos estáticos eram mantidos em cache de forma agressiva
- Limpeza de cache não está disponível
- Deploys aparecem como “bem-sucedidos” mas as mudanças não eram visíveis
Então eu estava presa com um cache que eu não conseguia limpar e mudanças que eu precisava validar em algum lugar além da minha máquina local. Foi aí que eu percebi que era hora de versionar os scripts de estilo.
Arquivos estáticos versionados
Uma forma confiável de contornar cache agressivo é mudar a URL do arquivo em cada deploy.
Arquivos estáticos são referenciados com uma versão específica do deploy, no meu caso eu escolhi o SHA curto do git.
<link rel="stylesheet" href="/static/css/base.css?v=6ea4bbe">
Isso foi o suficiente para resolver o problema:
- CDNs fazem cache por URL.
- Uma nova URL garante um cache miss.
- Sem dependência de purge APIs ou cache headers.
- Comportamento simples e determinístico.
Essa abordagem funciona bem para staging, onde correção importa mais do que eficiência de cache. Então meu próximo desafio foi: como colocar isso no meu deployment sem eu ter que editar as URLs manualmente toda vez?
Deploys de staging com controle por label
Eu não consigo lembrar de atualizar URLs manualmente toda vez, então ao invés de sofrer cada vez que meu CSS não atualizava de acordo, eu ajustei meu código e adicionei um passo nas minhas GitHub Actions para cuidar disso para mim.
Já que eu sou a única pessoa desenvolvendo nesse projeto, meus deploys de staging são explicitamente controlados usando labels de pull request.

Um pull request é colocado em staging apenas quando a label preview é aplicada.
GitHub Actions
Caso você queira replicar isso para você, aqui está como fazer. Os passos são bem simples:
- Rode seus testes;
- Se os testes passarem faça o deploy da aplicação para o ambiente de staging
- Faça um comentário no seu PR para que você saiba a versão do CSS que deveria estar no ar
- Aproveite o QA do seu deployment em staging
Primeiro a configuração para sua action:
name: Run tests and stage changes
on:
pull_request:
types:
- opened
- synchronize
- reopened
- labeled
- unlabeled
jobs:
# ...
Isso nomeia a action e diz a ela quais Pull Requests observar, no meu caso todos eles.
1. Rodando os testes
Eu quero ter certeza que todos os pull requests passam na minha suíte de testes, então o primeiro job faz checkout do pull request, instala as dependências e cria os arquivos necessários, e então roda os testes:
# ...
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout PR branch
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Install dependencies
run: uv sync --extra dev
- name: Setup test environment
run: cp .env.example .env
- name: Run tests
run: uv run pytest
deploy-staging:
# ...
Isso garante qualidade de código em todos os pull requests antes mesmo de eu considerar deployment.
Com um teste bem-sucedido podemos seguir para o deployment.
2. Deploy em staging
O job de deployment só precisa rodar quando a label preview está incluída.
E aí vem um truque legal: você pode pegar o SHA para o deployment com github.sha e escrever isso para um arquivo, nesse caso .deploy_sha e uma vez que o código é enviado para a nuvem ele pode usar esse arquivo para ler a informação.
jobs:
test:
# ...
deploy-staging:
# Only run after tests pass
needs: test
if: contains(github.event.pull_request.labels.*.name, 'preview')
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout PR branch
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Set deploy SHA
run: echo "$" | cut -c1-7 > .deploy_sha
- name: Deploy to staging
env:
CLOUD_TOKEN: $
CLOUD_APP_ID: $
run: # your deploy command here
# ...
Meu código também precisou levar isso em conta. Então primeiro eu criei uma função na minha aplicação FastAPI para pegar os primeiros caracteres do SHA da variável de ambiente ou do arquivo .deploy_sha. Eu também defini um fallback para dev.
# Deploy SHA for cache busting - check env var first, then file, fallback to "dev"
def get_deploy_sha():
"""Get deploy SHA from environment or .deploy_sha file."""
sha = os.getenv("DEPLOY_SHA")
if sha:
return sha[:7]
# Try reading from file (created during CI/CD deploy)
try:
with open(".deploy_sha", "r") as f:
return f.read().strip()
except FileNotFoundError:
return "dev"
# Automatically make the deploy_sha available in all templates
templates.env.globals["deploy_sha"] = get_deploy_sha()
A variável de ambiente é usada em produção, que normalmente não muda a não ser que eu veja algum cache estranho que eu não esteja esperando. Enquanto isso, quando eu estou desenvolvendo localmente o fallback entra em ação, e em staging nós usamos o arquivo.
Finalmente o template HTML fica assim:
<link rel="stylesheet" href="/static/css/base.css?v=">
Já que passamos o deploy_sha automaticamente como parte das variáveis globais para templates, qualquer página vai ter a informação que ela precisa quando a aplicação está sendo construída.
3. Receber um comentário
Finalmente, eu queria receber um comentário onde eu pudesse ver ambos:
- Que o deployment está no ar
- O hash que eu deveria procurar caso eu perceba algumas discrepâncias entre o que eu estou vendo e o deployment
Para isso eu adicionei um passo final ao job deploy-staging com o seguinte código:
# ...
jobs:
test:
# ...
deploy-staging:
# ...
steps:
# ...
- name: Comment staging URL on PR
uses: actions/github-script@v7
with:
script: |
const sha = '$'.substring(0, 7);
const marker = '<!-- staging-deploy -->';
const body = `${marker}\n🚀 **Staging deploy complete**\n\nPreview: $\n\nCommit: \`${sha}\``;
// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body,
});
}
Já que eu também não queria que cada novo commit gerasse um novo comentário, eu usei um comentário HTML para marcar a mensagem:
const marker = '<!-- staging-deploy -->';
Já que comentários renderizam Markdown, o HTML aparece no modo de edição mas fica escondido quando é exibido.

E é assim que fica quando o passo final do deploy-staging é completado:

Prós e contras dessa abordagem
Isso me deu controle total sobre qual pull request está em staging e meu staging representa “atualmente em revisão” tornando mais fácil até para mim fazer o QA das mudanças.
O único lado negativo dessa abordagem é que eu não posso ter múltiplos ambientes efêmeros por pull request já que eu posso fazer apenas um deployment de PR por vez, mas isso funciona bem para meu fluxo de trabalho de desenvolvimento.
O objetivo é clareza e controle, não automação máxima.
Conclusão
Arquivos estáticos versionados e deploys com controle por label resolveram meu problema de cache em staging. Agora arquivos CSS recebem um SHA do git na URL, GitHub Actions cuida dos deployments quando eu aplico a label preview, e eu sempre sei qual versão está no ar. Sem necessidade de invalidação de cache.