feat(release): deploy por tag estrita com garantias, tag-watcher e verify-live [INFRA-67970] #29

Merged
tarcisio merged 5 commits from feat/INFRA-67970-release-deploy-inputs into main 2026-10-01 13:30:34 -03:00
Owner

O quê

Reescreve o release único do ci-shared para o gatilho por tag estrita, com garantias de que o que está no ar é a tag (decisões do operador, 01/10/2026). Cobre o piloto DANI (v3.2.0 espera este pipeline) e os demais produtos do deploy padrão (host tmf-prod); TMF7/TMF9 (EKS) ficam fora.

  • Gatilho único: push de tag vX.Y.Z estrita + workflow_dispatch(version) de reserva. A tag nasce fora do workflow (token de workflow não dispara workflow no Forgejo — TH-54635). Todo deploy sai de main; hotfix = PR para main + nova tag.
  • PRÉ (antes de qualquer SSH/build): tag existe, commit ancestral de origin/main, bate com o SHA do evento (tag movida = falha), VERSION do commit == tag; no host, git describe --exact-match == tag.
  • BUILD: APP_VERSION (tag sem v) e APP_GIT_SHA exportados ao deploy_script.
  • PÓS: health interno e público; com deploy_verify_version: true, version == tag e git_sha == sha, senão rollback automático + job vermelho.
  • REGISTRO: commit status deploy/production no sha da tag (sempre, sucesso ou falha) + marcador .deployed no host.
  • Lock do host: flock em /var/lock/deploy-<hostname>.lock (a concurrency do Forgejo é por repositório).
  • Reserva do push de tag: verify-live.yml agendado — tag-watcher (dispara dispatch(version) da tag que ficou sem status nem run, idempotente) e verify-live (compara o ar × último status success).
  • Opcionais: deploy_timeout_minutes (10–240), backmerge main→develop (desligado por padrão).
  • Corrige uma falha de segurança de defesa em profundidade: deploy-remoto.sh aceitava ROLLBACK_SCRIPT/DEPLOY_SCRIPT com caminho absoluto (/etc/passwd seria executado com bash).

⚠ Muda comportamento

  1. rollback_script passa a ser chamado SEM argumentos, com PREV_TAG no ambiente. O rollback.sh do DANI espera --yes <tag>: precisa ler PREV_TAG do ambiente.
  2. RELEASE_TOKEN é OBRIGATÓRIO no Infisical (deploy_infisical_project, prod, pasta deploy_infisical_path): publica o status e permite o watcher disparar workflow. Sem ele o deploy falha fechado antes de tocar no host. O projeto do DANI tem 0 segredos em prod (tabela do ORG, 25/09): o operador precisa criar DEPLOY_HOST, DEPLOY_USER, SSH_PRIVATE_KEY, SSH_KNOWN_HOSTS, RELEASE_TOKEN (e DEPLOY_PORT, opcional) antes do 1º deploy.
  3. Chamador novo + 2º chamador. templates/release-chamador.yml mudou (push de tag + dispatch(version); input tag virou version; sai o gatilho de push em main). Novo templates/verify-live-chamador.yml. O produto copia os dois sem editar (scripts/conferir-ci-identico.sh reprova cópia editada) e deve apagar o deploy/auto-tag antigos no mesmo PR.
  4. deploy_verify_version é opt-in (padrão false): enquanto o produto não expõe version+git_sha no /health, confere só 2xx e o status sai "versão não verificada".
  5. Novas chaves do ci-config: deploy_verify_version, deploy_version_field, deploy_sha_field, deploy_image_repo, deploy_infisical_project (padrão = infisical_project; apontar para INFRA compartilha a chave SSH — decisão do operador), deploy_tag_grace_minutes, deploy_lock_timeout_seconds, deploy_timeout_minutes, deploy_backmerge(_para).

Como testar

for d in tests/*/; do python3 -m unittest discover -s "$d"; done   # na imagem tmf-ci-python:3.12

Verificado (imagem real tmf-ci-python:3.12, linux/amd64, depois do rebase sobre main com o #28): 301 testes, rc=0 — release 168, ci-config 67, ci-unico 39, hub-issue-sync 18, infisical-secrets 9. Os testes usam docker/ssh/Forgejo falsos e um clone git real; o timeout foi provado com ssh falso (estouro = rc 124).
Mutações reprovadas pelos testes certos: sem o teste de ancestral de main; sem a comparação de versão; watcher filtrando por event; watcher ignorando status existente; data do commit em vez da data da tag.

NÃO provado: nada rodou no act_runner

  • o release.yml novo, o schedule, o workflow_dispatch e o runs-on não foram executados num runner real;
  • nenhum deploy em host real; o deploy_script de cada produto precisa propagar APP_VERSION/APP_GIT_SHA (não verificado);
  • RELEASE_TOKEN com permissão real (write:repository) não foi exercitado;
  • runs-on: docker ficou fixo: expressão em runs-on não é provada e, se falhar, quebraria todo deploy.

Plano de prova em QA (pelo DANI)

  1. Operador cria os 5 segredos em prod:/deploy do projeto do DANI e confere (secret.sh --doctor).
  2. DANI copia os dois chamadores, ajusta o ci-config e o rollback.sh (PREV_TAG), e roda primeiro em QA (deploy_verify_version: false).
  3. Tag de teste → conferir: run nasceu, status deploy/production publicado, marcador .deployed no host.
  4. Provar o watcher: empurrar uma tag e impedir o disparo; esperar a carência; o verify-live deve disparar o dispatch.
  5. Ligar deploy_verify_version: true no QA, provocar divergência (versão errada) e ver rollback + vermelho.
  6. Só então tag real v3.2.0 em produção.

Fora de escopo (fatias seguintes)

Ambientes prod|qa|dev parametrizados + restore-db.yml (fatia 3) e capture-releases (fatia 4).

Refs: INFRA-67970

🤖 Generated with Claude Code

https://claude.ai/code/session_01G2e3V17tRc4mAZ4ea1XX1e

## O quê Reescreve o release único do `ci-shared` para o **gatilho por tag estrita**, com garantias de que **o que está no ar é a tag** (decisões do operador, 01/10/2026). Cobre o piloto DANI (v3.2.0 espera este pipeline) e os demais produtos do deploy padrão (host `tmf-prod`); TMF7/TMF9 (EKS) ficam fora. - **Gatilho único:** push de tag `vX.Y.Z` estrita + `workflow_dispatch(version)` de reserva. A tag nasce **fora do workflow** (token de workflow não dispara workflow no Forgejo — TH-54635). Todo deploy sai de `main`; hotfix = PR para `main` + nova tag. - **PRÉ** (antes de qualquer SSH/build): tag existe, commit **ancestral de `origin/main`**, bate com o SHA do evento (tag movida = falha), `VERSION` do commit == tag; no host, `git describe --exact-match` == tag. - **BUILD:** `APP_VERSION` (tag sem `v`) e `APP_GIT_SHA` exportados ao `deploy_script`. - **PÓS:** health interno e **público**; com `deploy_verify_version: true`, `version` == tag e `git_sha` == sha, senão **rollback automático** + job vermelho. - **REGISTRO:** commit status `deploy/production` no sha da tag (sempre, sucesso ou falha) + marcador `.deployed` no host. - **Lock do host:** `flock` em `/var/lock/deploy-<hostname>.lock` (a `concurrency` do Forgejo é por repositório). - **Reserva do push de tag:** `verify-live.yml` agendado — **tag-watcher** (dispara `dispatch(version)` da tag que ficou sem status nem run, idempotente) e **verify-live** (compara o ar × último status `success`). - Opcionais: `deploy_timeout_minutes` (10–240), backmerge `main→develop` (**desligado** por padrão). - Corrige uma falha de segurança de defesa em profundidade: `deploy-remoto.sh` aceitava `ROLLBACK_SCRIPT`/`DEPLOY_SCRIPT` com **caminho absoluto** (`/etc/passwd` seria executado com `bash`). ## ⚠ Muda comportamento 1. **`rollback_script` passa a ser chamado SEM argumentos, com `PREV_TAG` no ambiente.** O `rollback.sh` do DANI espera `--yes <tag>`: precisa ler `PREV_TAG` do ambiente. 2. **`RELEASE_TOKEN` é OBRIGATÓRIO no Infisical** (`deploy_infisical_project`, `prod`, pasta `deploy_infisical_path`): publica o status e permite o watcher disparar workflow. Sem ele o deploy **falha fechado** antes de tocar no host. O projeto do DANI tem **0 segredos em `prod`** (tabela do ORG, 25/09): o operador precisa criar `DEPLOY_HOST`, `DEPLOY_USER`, `SSH_PRIVATE_KEY`, `SSH_KNOWN_HOSTS`, `RELEASE_TOKEN` (e `DEPLOY_PORT`, opcional) antes do 1º deploy. 3. **Chamador novo + 2º chamador.** `templates/release-chamador.yml` mudou (push de tag + `dispatch(version)`; input `tag` virou `version`; sai o gatilho de push em `main`). Novo `templates/verify-live-chamador.yml`. O produto copia **os dois sem editar** (`scripts/conferir-ci-identico.sh` reprova cópia editada) e deve apagar o deploy/auto-tag antigos **no mesmo PR**. 4. `deploy_verify_version` é **opt-in** (padrão `false`): enquanto o produto não expõe `version`+`git_sha` no `/health`, confere só 2xx e o status sai "versão não verificada". 5. Novas chaves do `ci-config`: `deploy_verify_version`, `deploy_version_field`, `deploy_sha_field`, `deploy_image_repo`, `deploy_infisical_project` (padrão = `infisical_project`; apontar para INFRA compartilha a chave SSH — decisão do operador), `deploy_tag_grace_minutes`, `deploy_lock_timeout_seconds`, `deploy_timeout_minutes`, `deploy_backmerge(_para)`. ## Como testar ``` for d in tests/*/; do python3 -m unittest discover -s "$d"; done # na imagem tmf-ci-python:3.12 ``` **Verificado** (imagem real `tmf-ci-python:3.12`, linux/amd64, depois do rebase sobre `main` com o #28): **301 testes, rc=0** — `release` 168, `ci-config` 67, `ci-unico` 39, `hub-issue-sync` 18, `infisical-secrets` 9. Os testes usam `docker`/`ssh`/Forgejo **falsos** e um clone git real; o timeout foi provado com `ssh` falso (estouro = rc 124). **Mutações reprovadas pelos testes certos:** sem o teste de ancestral de `main`; sem a comparação de versão; watcher filtrando por `event`; watcher ignorando status existente; data do commit em vez da data da tag. ## NÃO provado: nada rodou no act_runner - o `release.yml` novo, o `schedule`, o `workflow_dispatch` e o `runs-on` **não foram executados** num runner real; - nenhum deploy em host real; o `deploy_script` de cada produto precisa propagar `APP_VERSION`/`APP_GIT_SHA` (não verificado); - `RELEASE_TOKEN` com permissão real (`write:repository`) não foi exercitado; - `runs-on: docker` ficou **fixo**: expressão em `runs-on` não é provada e, se falhar, quebraria todo deploy. ## Plano de prova em QA (pelo DANI) 1. Operador cria os 5 segredos em `prod:/deploy` do projeto do DANI e confere (`secret.sh --doctor`). 2. DANI copia os dois chamadores, ajusta o `ci-config` e o `rollback.sh` (`PREV_TAG`), e roda **primeiro em QA** (`deploy_verify_version: false`). 3. Tag de teste → conferir: run nasceu, status `deploy/production` publicado, marcador `.deployed` no host. 4. Provar o **watcher**: empurrar uma tag e impedir o disparo; esperar a carência; o `verify-live` deve disparar o `dispatch`. 5. Ligar `deploy_verify_version: true` no QA, provocar divergência (versão errada) e ver **rollback + vermelho**. 6. Só então tag real `v3.2.0` em produção. ## Fora de escopo (fatias seguintes) Ambientes `prod|qa|dev` parametrizados + `restore-db.yml` (fatia 3) e `capture-releases` (fatia 4). Refs: INFRA-67970 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01G2e3V17tRc4mAZ4ea1XX1e
Fatia 1 do INFRA-67970. As chaves do DANI (dirs, health, URL publica, expected_host,
script, rollback) ja existiam no ci-config; faltavam:

- deploy_timeout_minutes (10..240, padrao 60): prazo do passo de deploy, aplicado com
  'timeout' no proprio passo (expressao em timeout-minutes nao e provada no act_runner);
  teto do job = 240. Estouro = rc 124 com mensagem 'CONFERIR O HOST'.
- deploy_backmerge / deploy_backmerge_para (padrao false / develop): scripts/backmerge.sh
  abre PR chore/backmerge-<tag> main->develop DEPOIS de deploy bem-sucedido. Passo do job
  deploy (sem if de job), nunca faz merge, idempotente, FALHA se o PR nao abrir.
- ci_config.py passa a aplicar 'maximum' do schema (so 'minimum' era aplicado).

Prova: 5 suites na imagem tmf-ci-python:3.12 — release 83 OK (11 novos do backmerge.sh
contra API falsa + 7 de contrato), ci-config 67 OK. Timeout provado com ssh falso: ok=0,
falha=3 preservado, 'abc'/'' rejeitados, estouro rc=124 em 60 s. Mutacao do script
(engolir falha do PR) reprova exatamente 1 teste. ci-unico: 3 falhas PRE-EXISTENTES em
main (corrida #25 x #26), corrigidas no PR #28.
NAO provado: act_runner executando o passo; backmerge contra Forgejo real.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2e3V17tRc4mAZ4ea1XX1e
Fatia 2a (o release.yml ainda NAO chama estes argumentos novos: nao publicar antes da 2b).
scripts/deploy-remoto.sh, modo deploy:
- PRE: git describe --exact-match --tags == TAG e commit ancestral de origin/BASE_BRANCH
  (todo deploy sai de main; hotfix = PR para main + tag). Reprova ANTES do build.
- BUILD: exporta APP_VERSION (tag sem v) e APP_GIT_SHA; PREV_TAG lido da imagem atual do
  container (IMAGE_REPO) e entregue ao rollback_script no ambiente.
- POS: com VERIFY_VERSION=true o health JSON tem de trazer VERSION_FIELD == versao e
  SHA_FIELD == sha da tag; divergencia = logs + rollback automatico + exit 1. Padrao false
  (opt-in ate o produto aderir; log e marcador dizem 'VERSAO NAO VERIFICADA').
- REGISTRO: $DEPLOY_DIR/.deployed (tag, sha, deployed_at, run_url, verified) so no sucesso;
  linhas PREV_TAG= LIVE_SHA= VERIFIED= parseaveis pelo workflow.
Novos argumentos validados (defesa em profundidade): BASE_BRANCH IMAGE_REPO VERIFY_VERSION
VERSION_FIELD SHA_FIELD RUN_URL. Compativel com os 11 testes antigos.

Prova (imagem tmf-ci-python:3.12): test_deploy_remoto 25 OK (14 novos). Mutacao sem o teste
de ancestral reprova test_tag_fora_de_main_aborta_antes_do_build; mutacao sem a comparacao
de versao reprova test_versao_divergente_reprova_e_faz_rollback.
NAO provado: execucao em host real; o script do produto propagar APP_VERSION/APP_GIT_SHA.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2e3V17tRc4mAZ4ea1XX1e
Fatia 2b. Reescreve o release.yml (decisoes do operador 01/10): GATILHO UNICO = push de tag
estrita vX.Y.Z + workflow_dispatch(version); a tag nasce FORA do workflow (token de workflow
nao dispara workflow no Forgejo, TH-54635); todo deploy sai de main (hotfix = PR p/ main + tag).

- release-tag.sh decidir: tag estrita existente, commit ancestral de origin/main, igual ao SHA do
  evento (tag movida = falha), VERSION do commit == tag. 'criar' vira ferramenta de linha de comando.
- release.yml: jobs tag/deploy (nomes fixos, sem if de job); checkout da propria tag; segredos do
  Infisical com RELEASE_TOKEN OBRIGATORIO e projeto deploy_infisical_project (padrao = o do produto);
  verificacao publica; rollback se ela reprovar; commit status deploy/production SEMPRE (sucesso ou
  falha); runs-on docker fixo (expressao em runs-on nao provada no act_runner).
- deploy-remoto.sh: lock do HOST (flock, serializa produtos), modo rollback validado, e recusa script
  com caminho absoluto (defesa em profundidade: ROLLBACK_SCRIPT=/etc/passwd executaria).
- deploy-status.sh e verify-publico.sh (novos, testados com servidores falsos).
- schema/ci_config/exemplo: deploy_verify_version (OPT-IN, false), deploy_version_field,
  deploy_sha_field, deploy_image_repo, deploy_infisical_project, deploy_tag_grace_minutes,
  deploy_lock_timeout_seconds. templates/release-chamador.yml: push de tag + dispatch(version).

Prova (imagem tmf-ci-python:3.12): release 131 OK, ci-config 67 OK, infisical-secrets 9, hub-issue-sync
18. ci-unico: 3 falhas PRE-EXISTENTES em main (corrida #25 x #26), corrigidas no PR #28.
NAO provado: act_runner executando o novo release.yml; deploy em host real; o deploy_script do produto
propagar APP_VERSION/APP_GIT_SHA; o watcher/verify-live (fatia 2c, ainda nao feita).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2e3V17tRc4mAZ4ea1XX1e
feat(release): tag-watcher e verify-live (reserva do push de tag + detector de drift) [INFRA-67970]
All checks were successful
PR checks (ci-shared) / testes (pull_request) Successful in 1m19s
337bfbaa80
Fatia 2c. O push de tag pode falhar em silencio (TH-54635: 3 de 8 no Hub), entao a reserva e MECANICA:
workflow agendado (a cada 15 min), que nao depende de push de tag.

- scripts/verify-live.py: watch = tag estrita mais recente (semver), carencia contada da data da TAG
  (tagger.date, nao a do commit), sem status deploy/production NEM run do release.yml => dispatch(version);
  idempotente (status/run existentes, ate de deploy que FALHOU, nao redisparam), tag fora de main nunca
  e disparada. compare = ar x ultimo success (version+sha com deploy_verify_version; senao so 2xx).
- Fatos medidos na API 15.0.9: runs?head_sha= filtra, 'limit' sem 'page' devolve TODOS os runs;
  'event' vem VAZIO em runs reais de tag (nao filtrar por evento); /git/tags/<id>.tagger.date.
- verify-live.yml (reusavel, 2 jobs independentes sem if de job, RELEASE_TOKEN do Infisical) e
  templates/verify-live-chamador.yml (cron */15); conferir-ci-identico.sh confere a copia identica.

Prova (imagem tmf-ci-python:3.12): release 168 OK (26 do verify-live.py contra Forgejo falso, 11 de
contrato do workflow/chamador). Mutacoes reprovadas pelos testes certos: filtrar por event, ignorar o
status existente, usar a data do commit em vez da data da tag. ci-unico: 3 falhas PRE-EXISTENTES (PR #28).
NAO provado: schedule/dispatch no act_runner; RELEASE_TOKEN com permissao real; dispatch contra Forgejo real.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2e3V17tRc4mAZ4ea1XX1e
fix(release): pending no inicio, already live, disk-guard, lock com dono, cron 6 h [INFRA-67970]
All checks were successful
PR checks (ci-shared) / testes (pull_request) Successful in 1m20s
c0d10b89f9
Corrige defeitos do proprio #29 achados ao ler o modelo da frota (07-critica) e ao medir a carga:

- Status 'pending' (deploying vX.Y.Z) no INICIO do deploy. O run de workflow_dispatch nasce com
  head_sha de main, nao o da tag, e o status final so sai no fim: um deploy longo era redisparado
  pelo watcher. Falha cedo se o RELEASE_TOKEN nao tem permissao (antes de tocar no host).
  Watcher: 'pending' com mais de PENDING_MAX_MIN (240) = deploy TRAVADO, libera novo dispatch
  mesmo com um run morto 'existente'; success/failure sempre contam como tratado.
- deploy-remoto.sh: idempotencia (.deployed == TAG@SHA + health ok [+ versao com verify] => 'already
  live', exit 0, sem build) e disk-guard (deploy_min_free_gb, padrao 15, no DockerRootDir; aborta sem
  tocar o slot ativo — tmf-qa esta a 96%, 42 GB livres).
- Lock do host grava pid/tag/run_url/hora; a mensagem de espera mostra o dono. Abre com >> para a
  espera nao truncar a identificacao de quem segura.
- Cron do verify-live: */15 -> 0 */6 * * * (decisao do operador D1). Medido: */15 somaria ~1.344
  jobs/dia (+70% das ~1.880 jobs de 24 h), 6 h soma ~84.
- Chave nova deploy_min_free_gb (schema, resolvedor, exemplo, README).

Prova (imagem tmf-ci-python:3.12): 315 testes rc=0 (release 182, ci-config 67, ci-unico 39,
hub-issue-sync 18, infisical-secrets 9). Teste de concorrencia agora prova B3: 2 deploys simultaneos
=> 1 build e o 2o sai 'already live'.
NAO provado: act_runner; dispatch com ref=refs/tags/<tag> (a mitigacao e o pending, nao o ref).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2e3V17tRc4mAZ4ea1XX1e
tarcisio deleted branch feat/INFRA-67970-release-deploy-inputs 2026-10-01 13:30:35 -03:00
Sign in to join this conversation.
No reviewers
No labels
seguranca
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
TMF/ci-shared!29
No description provided.