fix(deploy): compose referencia a tag qualificada do registry; F2 preserva por image ID (INF13)

O compose do stack referenciava a tag local bare teqo-1313:<sha>, que é uma
segunda ref que nada recria se perdida (achado INF8 24/08: só restava a ref
do registry e o 'compose up -d' falhou com No such image). Agora:

- swap escreve sempre localhost:5000/teqo-1313(-migrator):<sha> (sed
  idempotente aceita ambas as formas); os docker tag bare somem (sem
  consumidor);
- rollback do runbook simplifica (docker pull basta, sem dance de tag);
- limpeza pós-deploy (INF3/F2) preserva TODAS as tags da imagem em uso pelo
  IMAGE ID do container, imune a drift de label do compose.

Runbook docs/ops/teqo-1313-deploy.md atualizado (fluxo, rollback, tabela de
falhas com o achado). Issue de referência (tracker infra-solla):
git.solla.dev/fsolla/infra-solla#24
This commit is contained in:
Francisco Solla 2026-08-24 14:12:36 -03:00
parent ae79eb507c
commit ddd58a01d1
2 changed files with 45 additions and 35 deletions

View file

@ -22,15 +22,17 @@ homeserver, o job `verify` (hosted) roda a suíte **full** (incl. e2e full).
== SHA do job, senão "stale run" skip) → `flock` (serializa) → guard
"already deployed" (revision do container rodando) → **build do migrator**
(o estágio migrator não roda `next build` — builda mesmo contra o schema
antigo; BuildKit, secrets do `~/stack/teqo-1313.env`) → push/tag do
migrator em `localhost:5000` → swap dos tags de imagem no
`~/stack/docker-compose.yml` (backup antes) → **migrations**
antigo; BuildKit, secrets do `~/stack/teqo-1313.env`) → push do migrator em
`localhost:5000` (tag **qualificada de registry** — INF13: a única ref que o
compose referencia) → swap dos tags de imagem no
`~/stack/docker-compose.yml` (backup antes; sed idempotente aceita as formas
antiga e qualificada e escreve sempre a qualificada) → **migrations**
(`docker compose --profile maintenance run --rm teqo-1313-migrate </dev/null`,
já com a imagem do SHA novo) → **build do runner** (contra o banco JÁ
migrado — o `next build` da geração estática lê o schema novo, OPS66;
`--network host` com proxy socat `teqo-1313-build-proxy` na `stack_default`
para alcançar o `postgres`; o proxy é criado idempotentemente pelo script)
→ push/tag do runner → `docker compose up -d teqo-1313` → healthcheck →
→ push do runner (tag qualificada de registry) → `docker compose up -d teqo-1313` → healthcheck →
smoke (`/`, `/campanha/login`, `/admin`, barreira 307, WebAuthn
login-options, `api/revalidate` com o secret real).
5. Falha = job vermelho; nada é publicado pela metade (rollback automático
@ -103,9 +105,9 @@ cd ~/stack
# o backup do deploy que falhou é o compose com os tags antigos
ls docker-compose.yml.pre-* # escolha o anterior ao deploy ruim
cp docker-compose.yml.pre-<sha-anterior> docker-compose.yml
# 2. puxar do registry (a imagem antiga já não fica local após a limpeza):
# 2. puxar do registry (a imagem antiga já não fica local após a limpeza;
# o compose referencia a tag qualificada — INF13 — e o pull basta):
docker pull localhost:5000/teqo-1313:<sha-anterior>
docker tag localhost:5000/teqo-1313:<sha-anterior> teqo-1313:<sha-anterior>
# 3. subir:
docker compose up -d teqo-1313
```
@ -121,19 +123,20 @@ a correção se re-mergeia e o próximo deploy completo publica.
## Falhas conhecidas
| Sintoma | Causa | Tratamento |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Job verde sem deploy ("stale run") | Push mais novo venceu; o dispatch apontava um SHA que não é mais o HEAD do main do GitHub | Nada a fazer — dispatch novo no main atual |
| Job verde sem deploy ("already deployed") | O container rodando já tem a revision do SHA do job (ex.: `workflow_dispatch` duplicado da mesma HEAD) | Nada a fazer — o site já roda esse SHA. Forçar rebuild da MESMA HEAD não é suportado via dispatch: edite o tag da imagem no compose (`image: teqo-1313:<sha>-rebuild`) e `docker compose up -d`, ou espere um commit novo |
| Job `deploy` nunca roda / runner offline | Runner self-hosted do homeserver não instalado ou parado (passo manual do cutover OPS71) | Instalar/religar o runner (labels `self-hosted`, `homeserver`); os PRs e o `verify` não dependem dele — só o deploy |
| Deploy dispara mas o job `verify` falha | Regressão real na suíte full | Corrigir e re-dispatchar — o homeserver nunca é tocado |
| Deploy para logo após o migrate ("Done." e nada mais, EXIT=0) | `docker compose run` anexa stdin por padrão — o container consome o resto do script; o bash chega a EOF e termina sem rodar o rollout | O script já usa `< /dev/null` no `run --rm` do migrate (não remover); sintoma visto 2026-08-17 no primeiro deploy |
| Build falha: `network mode "stack_default" not supported by buildkit` | BuildKit (drivers docker e docker-container) recusa rede bridge custom no `--network` | O script builda com `--network host` + proxy socat `teqo-1313-build-proxy` (na `stack_default`, publicado em 127.0.0.1:5433, criado idempotentemente com `restart: unless-stopped`) + `DATABASE_URL` reescrita para o loopback |
| Build OOM no homeserver | Laptop 8c/16GB com o stack ativo (~12GB livres medidos) | Re-dispatch; se recorrente, item futuro: build na workstation com túnel |
| Build do runner falha: `relation "..." does not exist` no `next build` | Migration nova criou tabela lida em geração estática. Pré-OPS66 a ordem era build→migrate e o deploy morria aqui para sempre (incidente 2026-08-18, S2) | Pós-OPS66 não deve ocorrer: migrate roda antes do build do runner. Se reaparecer, confira no log se o passo migrate rodou; recovery manual: `docker build --target migrator` + `docker run --rm --network stack_default --env-file ~/stack/teqo-1313.env localhost:5000/teqo-1313-migrator:<sha>` e re-dispatch do workflow |
| Workspace clone sem o SHA novo | O workspace antigo apontava para o Forgejo local (pré-OPS71) e o fetch não achou o SHA | O script agora re-aponta `origin` para `TEQO_REPO_URL` (idempotente) antes do fetch — se ainda falhar, `git -C ~/teqo-deploy remote -v` e corrigir manualmente |
| Migrate falha | Drift/erro de schema | Job vermelho; site segue no container antigo; corrigir e re-mergear |
| Smoke falha pós-up | Regressão de runtime | Rollback automático (restore + `up -d`) + job vermelho; investigar |
| Sintoma | Causa | Tratamento |
| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Job verde sem deploy ("stale run") | Push mais novo venceu; o dispatch apontava um SHA que não é mais o HEAD do main do GitHub | Nada a fazer — dispatch novo no main atual |
| Job verde sem deploy ("already deployed") | O container rodando já tem a revision do SHA do job (ex.: `workflow_dispatch` duplicado da mesma HEAD) | Nada a fazer — o site já roda esse SHA. Forçar rebuild da MESMA HEAD não é suportado via dispatch: edite o tag da imagem no compose (`image: localhost:5000/teqo-1313:<sha>-rebuild`) e `docker compose up -d`, ou espere um commit novo |
| Job `deploy` nunca roda / runner offline | Runner self-hosted do homeserver não instalado ou parado (passo manual do cutover OPS71) | Instalar/religar o runner (labels `self-hosted`, `homeserver`); os PRs e o `verify` não dependem dele — só o deploy |
| Deploy dispara mas o job `verify` falha | Regressão real na suíte full | Corrigir e re-dispatchar — o homeserver nunca é tocado |
| `No such image` ao recriar o container fora de deploy (ex.: `compose up -d` pós-reboot/migração) | O compose referenciava uma tag local bare (`teqo-1313:<sha>`) que desapareceu sem recriação (achado INF8/INF13, 24/08: só restava a ref do registry) | Corrigido no INF13: o compose referencia a tag **qualificada** (`localhost:5000/teqo-1313:<sha>`), que todo build cria e o cleanup F2 preserva por image ID. Recovery do estado antigo: `docker pull localhost:5000/teqo-1313:<sha>` + `docker compose up -d` |
| Deploy para logo após o migrate ("Done." e nada mais, EXIT=0) | `docker compose run` anexa stdin por padrão — o container consome o resto do script; o bash chega a EOF e termina sem rodar o rollout | O script já usa `< /dev/null` no `run --rm` do migrate (não remover); sintoma visto 2026-08-17 no primeiro deploy |
| Build falha: `network mode "stack_default" not supported by buildkit` | BuildKit (drivers docker e docker-container) recusa rede bridge custom no `--network` | O script builda com `--network host` + proxy socat `teqo-1313-build-proxy` (na `stack_default`, publicado em 127.0.0.1:5433, criado idempotentemente com `restart: unless-stopped`) + `DATABASE_URL` reescrita para o loopback |
| Build OOM no homeserver | Laptop 8c/16GB com o stack ativo (~12GB livres medidos) | Re-dispatch; se recorrente, item futuro: build na workstation com túnel |
| Build do runner falha: `relation "..." does not exist` no `next build` | Migration nova criou tabela lida em geração estática. Pré-OPS66 a ordem era build→migrate e o deploy morria aqui para sempre (incidente 2026-08-18, S2) | Pós-OPS66 não deve ocorrer: migrate roda antes do build do runner. Se reaparecer, confira no log se o passo migrate rodou; recovery manual: `docker build --target migrator` + `docker run --rm --network stack_default --env-file ~/stack/teqo-1313.env localhost:5000/teqo-1313-migrator:<sha>` e re-dispatch do workflow |
| Workspace clone sem o SHA novo | O workspace antigo apontava para o Forgejo local (pré-OPS71) e o fetch não achou o SHA | O script agora re-aponta `origin` para `TEQO_REPO_URL` (idempotente) antes do fetch — se ainda falhar, `git -C ~/teqo-deploy remote -v` e corrigir manualmente |
| Migrate falha | Drift/erro de schema | Job vermelho; site segue no container antigo; corrigir e re-mergear |
| Smoke falha pós-up | Regressão de runtime | Rollback automático (restore + `up -d`) + job vermelho; investigar |
## Backup passivo no Forgejo (OPS76-FOLLOWUP, 2026-08-21)

View file

@ -11,11 +11,12 @@
# Flow: HEAD guard (only the current main HEAD deploys) -> flock
# serialization -> workspace fetch at <sha> -> docker login (local registry)
# -> build of the MIGRATOR stage (it never runs `next build`, so it builds
# even against the old schema) -> push/tag of the migrator -> compose
# image-tag swap (with backup) -> migrate via the maintenance service
# even against the old schema) -> push of the migrator (registry-qualified
# tag — INF13: the ONLY ref the compose references) -> compose image-tag swap
# (with backup) -> migrate via the maintenance service
# (BEFORE the runner build — static generation reads the NEW schema, OPS66)
# -> build of the runner stage (BuildKit secrets, compose network) ->
# push/tag -> compose up -> healthcheck wait -> smoke. Any failure after the
# push -> compose up -> healthcheck wait -> smoke. Any failure after the
# swap rolls back to the previous compose + image; failures before it leave
# the running site untouched and the job red.
#
@ -151,9 +152,13 @@ cd "$WORKSPACE_DIR"
# and therefore needs the migrations applied first (OPS66).
build_image migrator "localhost:5000/teqo-1313-migrator:$SHA"
# The compose references bare `teqo-1313:<sha>` tags with pull_policy: never.
# INF13: the compose references REGISTRY-QUALIFIED tags
# (`localhost:5000/teqo-1313(-migrator):<sha>`) with pull_policy: never — the
# exact name every build produces, so the ref the compose needs can never go
# missing (the old bare `teqo-1313:<sha>` alias was a second local-only ref
# that nothing recreated once lost — homeserver incident 24/08, `No such
# image` on recreate).
docker push "localhost:5000/teqo-1313-migrator:$SHA"
docker tag "localhost:5000/teqo-1313-migrator:$SHA" "teqo-1313-migrator:$SHA"
# --- compose swap (backup first; failures from here roll back) ----------
@ -161,12 +166,12 @@ compose="$STACK_DIR/docker-compose.yml"
backup="$compose.pre-$SHA"
cp "$compose" "$backup"
sed -i -E \
-e "s|image: teqo-1313-migrator:[0-9a-f]+|image: teqo-1313-migrator:$SHA|g" \
-e "s|image: teqo-1313:[0-9a-f]+|image: teqo-1313:$SHA|g" \
-e "s|image: (localhost:5000/)?teqo-1313-migrator:[0-9a-f]+|image: localhost:5000/teqo-1313-migrator:$SHA|g" \
-e "s|image: (localhost:5000/)?teqo-1313:[0-9a-f]+|image: localhost:5000/teqo-1313:$SHA|g" \
-e "s|org.opencontainers.image.revision: [0-9a-f]+|org.opencontainers.image.revision: $SHA|g" \
"$compose"
grep -q "image: teqo-1313:$SHA" "$compose" || fatal "compose swap failed (runner image)"
grep -q "image: teqo-1313-migrator:$SHA" "$compose" || fatal "compose swap failed (migrator image)"
grep -q "image: localhost:5000/teqo-1313:$SHA" "$compose" || fatal "compose swap failed (runner image)"
grep -q "image: localhost:5000/teqo-1313-migrator:$SHA" "$compose" || fatal "compose swap failed (migrator image)"
rollback() {
trap - ERR
@ -197,7 +202,6 @@ cd "$WORKSPACE_DIR"
say "building runner"
build_image runner "localhost:5000/teqo-1313:$SHA"
docker push "localhost:5000/teqo-1313:$SHA"
docker tag "localhost:5000/teqo-1313:$SHA" "teqo-1313:$SHA"
# --- rollout ------------------------------------------------------------
@ -236,20 +240,23 @@ echo "$body" | grep -q '"revalidated":true' || smoke_fail "POST /api/revalidate
# antigas). Best-effort e APÓS o smoke: falha de limpeza nunca falha um
# deploy verde. O registry localhost:5000 preserva as imagens (rollback
# intacto — runbook teqo-1313-deploy.md); removemos só as tags LOCAIS.
# Fail-closed: sem revision no container, não remove nada além do cache.
# INF13: a preservação é pelo IMAGE ID do container em uso (`{{.Image}}`),
# não por comparação tag×label do compose — imune a drift de label/recriação
# manual; TODAS as tags da imagem rodando sobrevivem, as demais são removidas.
# Fail-closed: container sem image ID não remove nada além do cache.
say "post-deploy cleanup: build cache + tags locais antigas"
docker builder prune -f >/dev/null 2>&1 || true
in_use="$(docker inspect -f '{{index .Config.Labels "org.opencontainers.image.revision"}}' teqo-1313 2>/dev/null || true)"
if [ -n "$in_use" ]; then
in_use_id="$(docker inspect -f '{{.Image}}' teqo-1313 2>/dev/null || true)"
if [ -n "$in_use_id" ]; then
for img in $(docker images --format '{{.Repository}}:{{.Tag}}' | grep -E '^(localhost:5000/)?teqo-1313(-migrator)?:' || true); do
tag="${img##*:}"
[ "$tag" = "$in_use" ] && continue
img_id="$(docker image inspect -f '{{.Id}}' "$img" 2>/dev/null || true)"
[ -n "$img_id" ] && [ "$img_id" = "$in_use_id" ] && continue
docker rmi "$img" >/dev/null 2>&1 || true
done
else
say "cleanup: container sem revision — mantendo imagens locais"
say "cleanup: container sem image ID — mantendo imagens locais"
fi
say "deploy of $SHA complete: $(docker inspect -f '{{.Image}}' teqo-1313)"