CI/CD — GitHub Actions
Todos os repositórios da org cactus-agents usam GitHub Actions para CI e deploy.
Esta página é a referência técnica do pipeline: inventário de workflows, o que
o reusable de deploy faz passo a passo, e as mecânicas de multi-account.
:::tip Onde está o quê
- Como fazer (adicionar environment / brand / conta CF, purgar cache) → seção Deploy.
- Nomes de secrets e variáveis → Referência de secrets.
- Playbooks de operação de cache → Cache operations. :::
Inventário de workflows
front-cactus-core (SDK)
| Workflow | Trigger | O que faz |
|---|---|---|
ci-release.yml | pull_request (todos) + push em main | Jobs ci → changeset → release. Em PR só roda ci. Em push na main, auto-gera changeset dos conventional commits, versiona e publica |
delete-merged-branch.yml | pull_request: [closed] | Limpeza de branches mergeadas |
São exatamente dois arquivos. ci.yml foi deletado e release.yml renomeado
para ci-release.yml; auto-changeset.yml foi absorvido pelo mesmo arquivo.
front-web-base (template)
| Workflow | Trigger | O que faz |
|---|---|---|
ci-deploy.yml | push e pull_request nos globs main, main-*, stage, stage-*, sports, sports-* + workflow_dispatch | Jobs quality, config-lint (só PR), resolve-matrix, deploy (chama o reusable do front-ops) |
list-environments.yml | workflow_dispatch | Viewer read-only da topologia de envs declarada no front-ops. Sem build, sem deploy |
lighthouse.yml | workflow_dispatch (input brand) | Chama front-ops/lighthouse-base.yml. Manual-only — perf budget ainda não é gate de deploy |
delete-merged-branch.yml | pull_request: [closed] | Limpeza de branches mergeadas |
:::info Workflow único
CI e deploy do front-web-base ficam no mesmo arquivo ci-deploy.yml. O job
quality roda em todo trigger; deploy só fora de PR e quando a matriz resolve
algum env.
:::
Os globs de branch — leia os arrays, não o comentário
O ci-deploy.yml declara os filtros em dois arrays idênticos, e são eles que o
GitHub casa:
on:
pull_request:
branches: ['main', 'main-*', 'stage', 'stage-*', 'sports', 'sports-*']
push:
branches: ['main', 'main-*', 'stage', 'stage-*', 'sports', 'sports-*']
Acima deles há um comentário resumindo como "namespaces suportados: main*,
stage* e sports*". As duas formas descrevem a mesma coisa — o comentário é o
resumo em 3 namespaces, os arrays são os 6 valores operativos. Quando você
precisar adicionar cobertura, edite os arrays; é a forma que a mensagem de erro do
config-lint também pede ("Add a matching pattern … e.g. main-*").
:::caution dev* e performance* foram removidos dos filtros de branch
Os prefixos dev* e performance* saíram dos dois arrays em 2026-08-10,
junto com as 7 envs que os usavam — o commit e920c2b no front-ops removeu
os grupos dev, performance, react-c4ctus, stage-cache e stage-spa, e
moveu 7wins-cc de dev-7wins para main-7wins.
O próprio ci-deploy.yml carrega o aviso in-file: não recoloque o glob sem criar
a env correspondente no front-ops, senão o config-lint passa a acusar branch
sem env.
:::
front-ops (deploy e cache centralizados)
Onze workflows. Cinco expõem workflow_call (consumíveis por outros workflows —
deploy, deploy-docs, deploy-affiliates, lighthouse-base e purge-all); o
resto é operação manual ou agendada dentro do próprio front-ops.
Workflow (name:) | Arquivo | Trigger | O que faz |
|---|---|---|---|
| Deploy (Reusable) | deploy.yml | workflow_call — input environment | Reusable de deploy dos brand workers. Jobs check + deploy |
| Deploy Docs (Reusable) | deploy-docs.yml | workflow_call — inputs environment, ref (opcional) | Deploy do front-cactus-docs. Envs em config/docs/environments/ |
| Deploy Affiliates Front (Reusable) | deploy-affiliates.yml | workflow_call — inputs environment, ref (opcional) | Deploy do front de afiliados. Envs em config/affiliates/environments/ |
| Lighthouse CI (front-web-base) | lighthouse-base.yml | workflow_call — input brand (default state77-com); secrets GH_PACKAGES_TOKEN, FRONT_GH_ACTIONS_TOKEN (ambos opcionais) | Builda a brand, sobe preview Wrangler local e roda Lighthouse mobile |
| Bump cache generation | bump-cache-generation.yml | workflow_dispatch — reason, target_value, brand_workers (none/single/all), single_environment, redeploy_service_api | Auto-incrementa o token de generation, commita em config/cache/generation.yml e dispara os redeploys |
| Manual Cache Purge | manual-cache-purge.yml | workflow_dispatch — environment, tag_quickpick, tags, glob, segments, scope, dry_run | Purge manual por tag/glob/segmento em uma ou todas as camadas |
| Cache Status | cache-status.yml | workflow_dispatch — environment, include_state, tag, glob | Inspeção read-only do estado do cache de uma env |
| Purge All | purge-all.yml | workflow_dispatch e workflow_call — input environment | Purge total de uma env, em "two-pass reverse-order" (múltiplas rodadas, camadas em ordem inversa) |
| Purge Monitor | purge-monitor.yml | schedule: */5 * * * * + workflow_dispatch | Bate nas rotas de config/cache/monitor-routes.yml direto no BFF, compara o hash com o run anterior e, em drift, dispara purge-all.yml da brand |
| Sync purge options | sync-purge-options.yml | push em main nos paths de config + schedule: 0 9 * * * + workflow_dispatch | Regenera os dropdowns environment e tag_quickpick do manual-cache-purge.yml e abre uma PR se houver drift |
| Delete merged branch | delete-merged-branch.yml | pull_request: [closed] | Limpeza de branches mergeadas |
:::info Por que sync-purge-options abre PR em vez de commitar
Listas workflow_dispatch.options são YAML estático — o GitHub não as popula do
filesystem na hora de renderizar a UI. Além disso, o GitHub bloqueia por
hard-rule o app github-actions de alterar arquivos em .github/workflows/;
por isso o push da branch e a criação da PR usam o PAT FRONT_GH_ACTIONS_TOKEN.
Consequência prática: quando você adiciona uma env ou uma tag nova, aparece uma
PR automática atualizando os dropdowns — mergeie.
:::
Modelo de deploy: caller + reusable
┌──────────────────────────────┐ ┌──────────────────────────────────┐
│ front-web-base (caller) │ │ front-ops (single source) │
│ .github/workflows/ │ │ config/brands/web-base/ │
│ ci-deploy.yml │ │ brand.yml │
│ • quality │ ──────▶ │ cache.yml │
│ • config-lint (PR) │ chama │ environments/<env>/deploy.yml │
│ • resolve-matrix │ │ config/cache/*.yml │
│ • deploy (matrix) │ │ .github/workflows/deploy.yml │
└──────────────────────────────┘ └──────────────────────────────────┘
# ci-deploy.yml no caller — job que chama o reusable
deploy:
name: ${{ matrix.environment }}
needs: [quality, resolve-matrix]
if: needs.resolve-matrix.outputs.deploy_needed == 'true'
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.resolve-matrix.outputs.matrix) }}
uses: cactus-agents/front-ops/.github/workflows/deploy.yml@main
with:
environment: ${{ matrix.environment }}
secrets: inherit
O fork não contém lógica de resolução de worker_name, tokens Cloudflare,
política de cache ou pipeline de build/deploy. O único input do reusable é
environment — não existe input ref. Ver
branch governance.
Os jobs do caller
| Job | Quando roda | O que faz |
|---|---|---|
quality | sempre (PR draft é pulado; ready_for_review re-dispara) | pnpm install --frozen-lockfile (com NODE_AUTH_TOKEN: GH_PACKAGES_TOKEN) → lint → check → typecheck:ci → test com CACTUS_FORCE_REGISTRY=1 |
config-lint | só em pull_request não-draft | Ver abaixo |
resolve-matrix | needs: quality | push: todas as envs com default_branch == github.ref_name e auto_deploy: true. workflow_dispatch: uma entrada só. pull_request: pulado |
deploy | quando a matriz resolve ≥1 env | Chama o reusable, um job por env |
O que o config-lint valida
Três checagens, com severidades diferentes:
- Ownership da brand —
brand.yml::repodo diretório escaneado tem que ser igual ao repo caller. Divergência → erro. - Dropdown vs pastas de environment — assimétrico, de propósito:
- entrada no dropdown que não existe no
front-ops→ erro sempre (um dispatch dela quebraria); - env do
front-opsfaltando no dropdown → erro só quandoGITHUB_BASE_REF == main; em qualquer outra base branch é warning. Motivo declarado no arquivo: envs novas nascem namaine as branches longas herdam no próximo sync — antes disso, a checagem estrita quebrava toda PR dessas branches.
- entrada no dropdown que não existe no
- Cobertura dos globs de branch — todo
default_branchdeclarado nofront-opstem que casar com ao menos um glob deon.push.branchese deon.pull_request.branches. Sem cobertura → erro. Globs que não casam com nada são aceitos (flexibilidade intencional).
:::caution O brand-dir é hardcoded no caller
No front-web-base, o job config-lint fixa
BRAND_DIR="_ops/config/brands/web-base" e
CALLER="cactus-agents/front-web-base" no shell do step. Um fork tem que ajustar
esses valores para a própria brand — ver Adicionar brand/fork.
:::
O pipeline do reusable deploy.yml, passo a passo
Dois jobs: check (resolve e autoriza) e deploy (33 steps nomeados).
Job check
- Checkout ops repo (configs de brand).
- Resolve deploy config from brand configs — acha a brand cujo
repo:bate com o caller, lêenvironments/<env>/deploy.ymlcom fallback embrand.yml::defaultse nos defaults embutidos. Falha nas condições listadas em Deploy → Regras de bloqueio. - Authorization gate (workflow_dispatch) — só quando o evento é
workflow_dispatchedeploy_protection.required_teamestá setado. Verifica membership do ator no team via API.pushnunca é gateado. - Verify auto-deploy eligibility (push events) — só em
push.
Como o gate vive no job check, uma reprovação aparece na UI como
check ❌ → deploy skipped.
Job deploy — configuração do job
timeout-minutes: 30 # era 15; o cutover opcional estende o job
concurrency:
group: deploy-${{ needs.check.outputs.worker_name }}
cancel-in-progress: false # run novo ESPERA na fila
permissions:
contents: read
packages: read
deployments: write
id-token: write # OIDC do collector AWS SSM
:::info Por que a serialização por worker existe
Adicionada depois do postmortem stage-7k de 2026-07. Sem ela, dois runs
sobrepostos do mesmo worker podem terminar fora de ordem — código velho
deployado por último e/ou BUILD_TS invertido em relação à ordem real, fazendo a
versão viva se auto-marcar stale no build-currency gate (SSR cache desligado
silenciosamente até o deploy seguinte). cancel-in-progress: false é
deliberado: nunca matar um wrangler deploy no meio.
:::
Steps que rodam sempre
| # | Step | Nota |
|---|---|---|
| 1 | Checkout caller repo | no deploy_ref resolvido |
| 2-4 | Setup pnpm / Setup Node / Install dependencies | Node 24, cache pnpm |
| 5 | Resolve CF credentials | cf_account → FRONT_{XX}_CF_ACCOUNT_ID / _CF_API_TOKEN, mascarados com ::add-mask:: |
| 6 | Resolve API_BASE_URL and CF_WORKER_KEY | Lê só API_BASE_URL dos bindings do Worker. CF_WORKER_KEY vem da org secret — ver aviso abaixo |
| 7 | Generate .env | BRAND_LANGUAGE, ORIGIN_DOMAIN, API_BASE_URL, CF_WORKER_KEY |
| 8 | Build | pnpm build com as vars acima + GITHUB_SHA |
| 9 | Checkout ops repo (for cache config) | |
| 10 | Resolve cache policy | defaults → brand → env |
| 12 | Patch wrangler.toml with runtime vars | injeta o bloco [vars] — ver Referência de secrets |
| 13 | Resolve R2 S3 API credentials | |
| 17 | Deploy to Cloudflare Workers | deploy --name <worker> --keep-vars |
| 24 | Verify single-version deployment (no stuck split) | checa a config na API CF; split confirmado falha o job |
| 25 | Probe served build id (observational orphan check) | checa o que o hostname serve: poll em /api/version comparando buildId. Env atrás de CF Access não é alcançável do runner → warning |
| 26-27 | Resolve post-deploy cache purge segments / Resolve cache purge secrets | |
| 31 | Purge SSR cache (CF zone purge) | |
| 32 | Warm up cache | curl paralelo, budget de 8s por path, em /, /casino, /casino/live, /cassino, /cassino/ao-vivo, /buscar, /promocoes, /api/version, /api/configurations, /api/appearance |
| 33 | Summary | brand, env, conta CF, URL, ref, cache |
:::danger CF_WORKER_KEY NÃO é lido dos bindings do Worker
O step Resolve API_BASE_URL and CF_WORKER_KEY extrai dos bindings apenas
API_BASE_URL. CF_WORKER_KEY é resolvido exclusivamente por indireção sobre as
org secrets (SECRET_{CF_ACCOUNT}_WORKER_KEY ← FRONT_{XX}_CF_WORKER_KEY) —
não há fallback para o binding. Se você configurar a chave só no Worker, o
build sai com CF_WORKER_KEY vazio e sem erro nenhum.
:::
Steps opt-in, com o campo que os liga
| Step | Gate |
|---|---|
| Provision KV namespace | política de cache tem algum recurso com storage.snapshot: kv |
| Ensure R2 bucket exists (idempotent) | credenciais R2 presentes. Roda wrangler r2 bucket create front-assets-archive || true |
| Sync assets to R2 (cross-deploy fallback) | credenciais R2 presentes. skip_asset_verify: true desliga só o verify HTTP |
| Purge service-api cache BEFORE deploy | serviceApiPurge.enabled na política de cache |
| Patch entry worker + Deploy entry worker to Cloudflare | deploy_entry_worker: true |
| Configure AWS credentials (warm collector) | run_warm_cutover: true |
| Run warm collector (AWS SSM front-web-warmup) | run_warm_cutover: true |
| Bump CACHE_GENERATION_SECRET (web-base + entry) | run_warm_cutover: true |
| Purge all — multi-camada (após o bump) | run_warm_cutover: true |
| Skip purge notice | skip_post_deploy_purge: true |
| Purge platform cache after deploy | purge secret resolvido e skip_post_deploy_purge != true |
| Purge service-api cache AFTER deploy | serviceApiPurge.enabled e purge secret resolvido |
Entry worker (deploy_entry_worker)
Com a flag ligada, o job também publica um segundo Worker enxuto
<worker_name>-entry a partir de wrangler.entry.toml do base:
- o binding de service
ORIGINé reescrito porsedpara apontar para o worker principal (service = "<worker_name>"); - o
[vars]do entry é lockstep com o principal, só no subconjunto que ele consome:BUILD_ID,BUILD_TS,CACHE_NAMESPACE,CACHE_GENERATIONe os quatroSSR_CACHE_*_TTL. Sem KV, sem policy, sem cron — o entry não usa platform-cache. O lockstep importa porque a chave de cache é composta a partir desses valores; - apontar o domínio para o worker
-entryé um passo manual separado. A flag só publica.
:::danger wrangler.entry.toml não existe em toda branch
O arquivo está presente hoje apenas na branch stage-spa-v2 do
front-web-base — que é exatamente o default_branch do único env com
deploy_entry_worker: true. Ligar a flag num env cujo default_branch não tem o
arquivo faz o step de patch falhar. Confirme que a branch tem o
wrangler.entry.toml antes de habilitar.
:::
Warm cutover (run_warm_cutover)
Ordem fixa: collector SSM → bump da generation → purge multi-camada.
aws-actions/configure-aws-credentials@v4assumeAWS_WARMUP_ROLE_ARNvia OIDC emeu-central-1.aws ssm send-command --document-name front-web-warmupcom--targets Key=tag:front-web-warmup,Values=<tenant>e--parameters buildId=<BUILD_ID>,tenant=<tenant>, e então faz poll de todas as invocações (até ~10min); qualquer status diferente deSuccessfalha o job. Otenantvem do campo homônimo dodeploy.ymlda env (default = nome da env).- Bump
CACHE_GENERATION_SECRETno worker principal e no entry — flipa a generation via secret, sem rebuild. - Purge all multi-camada logo depois, para limpar o conteúdo velho da borda.
Requisitos: o secret AWS_WARMUP_ROLE_ARN (role com
ssm:SendCommand / ssm:ListCommandInvocations nas instâncias com a tag
front-web-warmup=<tenant>), os org secrets
FRONT_{XX}_CACHE_PURGE_SECRET / _CF_WORKER_KEY, e o entry já deployado.
:::caution Dois "warm-up" diferentes
O step Warm up cache do deploy (curl nas rotas principais, sempre) não tem
nada a ver com o front-service-api-warmup (Worker separado, alimentado por
crons de GitHub Actions). Ver Serviços de infraestrutura.
:::
Multi-account Cloudflare
Cada environment aponta para uma conta CF via o campo cf_account no
environments/<env>/deploy.yml (ou como default em brand.yml). O valor é um
prefixo de 2-4 letras maiúsculas.
| Nome | Prefixo |
|---|---|
| Cactus | CT |
| Bluetec | BT |
| Teste | TS |
O workflow resolve FRONT_{PREFIXO}_CF_ACCOUNT_ID e
FRONT_{PREFIXO}_CF_API_TOKEN, mascarando os valores com ::add-mask::.
:::info Por que adicionar uma conta exige editar YAML
GitHub Actions não permite indexar secrets.* dinamicamente por nome. Cada
secret é exposta como uma env var nomeada (SECRET_CT_ID, SECRET_BT_TOKEN, …)
e o shell resolve por indireção (${!VAR_NAME}). Além do deploy.yml, cinco
outros workflows carregam o próprio registro de secrets por conta — o passo a
passo completo está em Adicionar conta CF.
:::
Secrets
O catálogo completo — nomes, quem consome, obrigatoriedade e comportamento na ausência — vive em Deploy → Referência de secrets. Resumo do que é org-level:
| Secret | Escopo | Uso |
|---|---|---|
GH_PACKAGES_TOKEN | PAT read:packages | Instalar @cactus-agents/* no CI dos callers (job quality) e no lighthouse-base.yml |
FRONT_GH_ACTIONS_TOKEN | PAT repo, read:packages, read:org | Checkout do front-ops (privado), install de pacotes no deploy, resolução do HEAD SHA via API, authorization gate de team, e os pushes do bump-cache-generation / sync-purge-options / ci-release do core |
FRONT_{CT,BT,TS}_* | por conta CF | Account ID, API token, worker key, purge secret, R2 |
AWS_WARMUP_ROLE_ARN | Role ARN (OIDC) | Único credential não-Cloudflare e não-GitHub do caminho de deploy. Só usado quando run_warm_cutover: true |
LHCI_GITHUB_APP_TOKEN | token do Lighthouse CI GitHub App | lighthouse-base.yml |
Acesso das org secrets
Ao criar um novo fork/repo, adicione-o ao Repository access de cada secret relevante (GitHub → cactus-agents → Settings → Secrets and variables → Actions). Sem isso o workflow do novo repo não vê a secret e o deploy/CI falha.
Permissões dos workflows
Declare o bloco permissions explicitamente. Sem ele, o GITHUB_TOKEN recebe
permissões padrão que podem não incluir packages: read:
jobs:
quality:
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
Adicionando CI/CD a um novo fork
Os workflows já vêm no fork. Checklist resumido (passo a passo completo em Deploy → Adicionar brand/fork):
- Criar a brand e o primeiro env no
front-ops:config/brands/<brand>/brand.yml(comrepo:) econfig/brands/<brand>/environments/<env>/deploy.yml(comcf_account). - Ajustar
BRAND_DIR/CALLERnos jobs doci-deploy.ymldo fork. - Org secrets: adicionar o novo repo ao Repository access de
GH_PACKAGES_TOKEN,FRONT_GH_ACTIONS_TOKENe dosFRONT_{XX}_CF_*da conta. - CF API Token: se a brand usa domínio novo, incluir a zona em Zone Resources do token da conta.
- Subir o Worker e configurar
API_BASE_URLnos bindings — ver Adicionar environment. - Push no
default_branch(ou dispatch manual) → workflows rodam.