mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-15 02:23:07 +00:00
- deploy.yml: trigger push em main + preflight guard (pula se há run queued/in_progress; waiting não conta) e job hosted requeue após deploy-staging (fora do grupo deploy-homeserver) - scripts/lib/deploy-trigger.mjs (decidePreflight/decideRequeue) + CLIs deploy-preflight/deploy-requeue; github-api ganha listWorkflowRuns/getBranchHead - pins: ciSkipInvariants reescrito (push + guard + dispatch + requeue fora do grupo), deployTrigger/githubApi novos; deployScript intocado - docs realinhados (AGENTS*, AGENT-OPS, README, runbook, rules/skills) + changelog Closes #980
534 lines
42 KiB
Markdown
534 lines
42 KiB
Markdown
# Runbook: deploy do site 1313 no homeserver (OPS53 + OPS71 + OPS103 + OPS104 — início automático, produção com review)
|
||
|
||
Desde a OPS104 o deploy **começa sozinho**: todo merge em `main` (evento
|
||
`push`) inicia o run do `deploy.yml` no head, e um segundo gatilho re-despacha
|
||
um run novo se `main` andar durante o verify/staging. O que continua manual é
|
||
a **publicação em produção**: o job `deploy-production` espera a aprovação do
|
||
environment `production`. Antes de tocar o homeserver, o job `verify` (hosted)
|
||
roda a suíte **full** (incl. e2e full) **uma vez** e gateia os dois alvos:
|
||
`deploy-staging` → aprovação do environment `production` →
|
||
`deploy-production` (OPS103).
|
||
|
||
## Gatilho e fluxo
|
||
|
||
1. **Início automático (OPS104):** merge em `main` → o run nasce no head; o
|
||
job `preflight` (hosted) pula o run quando já existe outro deploy
|
||
`queued`/`in_progress`. Um run aguardando aprovação de produção (`waiting`)
|
||
**não** conta: um merge nessa janela começa um run novo e os dois ficam
|
||
pendentes (o humano aprova qual promover). `workflow_dispatch` (GitHub →
|
||
Actions → **Deploy** → Run workflow, ref: `main`) segue como escape manual e
|
||
**não** passa pelo guard.
|
||
2. Job `preflight` (hosted, `scripts/deploy-preflight.mjs`): consulta os runs
|
||
do próprio workflow; sem deploy ativo → segue. Falha de API é fail-open
|
||
(o guard é dedupe, não gate de segurança).
|
||
3. Job `verify` (hosted `ubuntu-latest`, ~50 min): suíte full sem skips —
|
||
check-test-locations → lint → format → typecheck → knip → cycles → unit →
|
||
int (migrate+seed nos services) → build → e2e full. **Intocado pelas
|
||
OPS103/OPS104**: verifica o commit, não o alvo.
|
||
4. Se `verify` verde, o job `deploy-staging` (`needs: [verify]`,
|
||
`environment: staging`, sem reviewer) roda no runner self-hosted e executa
|
||
`TEQO_ENV=staging bash scripts/deploy-homeserver.sh <sha>` — publica o SHA
|
||
em `staging.jorgesolla1313.com.br` (container `teqo-staging`,
|
||
`127.0.0.1:1314`).
|
||
5. Com staging verde, o job `deploy-production` (`needs: [deploy-staging]`,
|
||
`environment: production`) **aguarda a aprovação do reviewer** e então
|
||
executa `TEQO_ENV=production bash scripts/deploy-homeserver.sh <sha>` —
|
||
publica o **mesmo SHA** em produção (`teqo-1313`, `127.0.0.1:1313`).
|
||
Staging vermelho = produção nunca roda; a corrida entre os dois é
|
||
serializada pela `concurrency: deploy-homeserver`.
|
||
6. **Requeue (OPS104):** com o staging verde, o job hosted `requeue`
|
||
(`scripts/deploy-requeue.mjs`, **fora** do grupo `deploy-homeserver` —
|
||
um run aguardando aprovação não pode bloquear o requeue) compara o head de
|
||
`main` com o SHA do run; se `main` andou durante o verify/staging, dispara
|
||
um run novo no head novo. Falha aqui é fail-red (job vermelho): staging
|
||
velho nunca fica silencioso.
|
||
7. O script, no **homeserver**: `flock` único (compose, workspace e registry
|
||
são compartilhados — um deploy por vez no host; o `concurrency` do
|
||
workflow serializa os runs e o lock cobre invocação manual; desde a OPS102
|
||
o run vai até o fim com o SHA do dispatch mesmo se `main` avançar durante o
|
||
verify; o workspace é clonado/atualizado de `TEQO_REPO_URL` — default
|
||
`https://github.com/fsolla/teqo.git`, público) →
|
||
guard "already deployed" (revision do container do ambiente) → **build do
|
||
migrator** (o estágio migrator não roda `next build` — builda mesmo contra o
|
||
schema antigo; BuildKit, secrets do env file do ambiente) → 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 ancorado ao serviço do
|
||
ambiente — a linha do outro ambiente não é tocada) → **migrations**
|
||
(`docker compose --profile maintenance run --rm teqo-<env>-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 por ambiente na `stack_default` para
|
||
alcançar o `postgres`; o proxy é criado idempotentemente pelo script) →
|
||
push do runner (tag qualificada de registry) → `docker compose up -d
|
||
teqo-<env>` → healthcheck → smoke (`/`, `/campanha/login`, `/admin`,
|
||
barreira 307, WebAuthn login-options, `api/revalidate` com o secret real do
|
||
ambiente).
|
||
8. Falha = job vermelho; nada é publicado pela metade (rollback automático
|
||
após o swap, **só do ambiente que falhou**).
|
||
|
||
**Primeiro deploy (verificação ao vivo):** o caminho de estreia do OPS104 é
|
||
mergear em `main` e observar o run nascer sozinho: log do `preflight`
|
||
(`should_deploy=true`), `verify` verde, staging publicado e `deploy-production`
|
||
aguardando aprovação. Para o cutover OPS71, confira no log do job
|
||
`deploy-production` o `set-url` do workspace (origem Forgejo → GitHub), o
|
||
`deployed_sha` lido do container e o guard "already deployed" num segundo run
|
||
do mesmo SHA. Na OPS103, staging primeiro (bootstrap abaixo), inspeção humana,
|
||
aprovação, produção.
|
||
|
||
## Onde roda cada coisa
|
||
|
||
| Máquina | Papel |
|
||
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| **GitHub hosted** (ubuntu) | CI de PR (`ci-pr.yml`, job `checks`) e job `verify` do `deploy.yml` (suíte full). Nunca toca o homeserver. |
|
||
| **homeserver** (8c/16GB) | todo o stack (`~/stack/docker-compose.yml`): postgres `teqo_1313`, forgejo, registry `localhost:5000`, cloudflared, `teqo-1313` na 127.0.0.1:1313 e `teqo-staging` na 127.0.0.1:1314 (OPS103; DB `teqo_staging`, env `~/stack/teqo-staging.env`, bucket `teqo-media-staging`); **runner self-hosted do GitHub** (labels `self-hosted`, `homeserver`; instalado 2026-08-19 — Issue #113 OPS71-INFRA; `~/actions-runner` v2.336.0, systemd user `actions.runner.fsolla-teqo.teqo-1313-runner.service` — `systemctl --user status/restart`); workspace `~/teqo-deploy` (clone de `github.com/fsolla/teqo`, público) |
|
||
| **workstation** | dev/agentes apenas — o runner do Forgejo foi **desligado** no cutover (o schedule do `ci.yml` antigo não tem mais razão; religar é reversível) |
|
||
|
||
Segredos **não** ficam no GitHub: o script sourceia o env file do ambiente
|
||
(`~/stack/teqo-1313.env` em produção, `~/stack/teqo-staging.env` em staging) e
|
||
`~/stack/.env` (chmod 600) no próprio homeserver. O repo GitHub é público — o
|
||
clone do workspace não precisa de credencial. No GitHub ficam apenas os
|
||
secrets de integração: `FORGEJO_API_TOKEN` (flips pós-merge) e
|
||
`CURSOR_API_KEY` (archive helper, dormente).
|
||
|
||
## Staging (OPS103)
|
||
|
||
Alvo real e descartável para validar o SHA (migração/build/rollout/smoke) antes
|
||
de produção. Mesmo run, mesmo `verify`, mesmo script — o que muda é
|
||
`TEQO_ENV=staging` e o environment `staging` (sem reviewer; a aprovação que
|
||
importa é a de `production`).
|
||
|
||
| Item | Staging | Produção |
|
||
| ----------- | ------------------------------------------------------------------------------ | ------------------------------------------ |
|
||
| Container | `teqo-staging` (127.0.0.1:1314) | `teqo-1313` (127.0.0.1:1313) |
|
||
| Migrator | `teqo-staging-migrate` | `teqo-1313-migrate` |
|
||
| DB | `teqo_staging` | `teqo_1313` |
|
||
| Env file | `~/stack/teqo-staging.env` | `~/stack/teqo-1313.env` |
|
||
| Imagens | `localhost:5000/teqo-staging(-migrator):$SHA` | `localhost:5000/teqo-1313(-migrator):$SHA` |
|
||
| Bucket | `teqo-media-staging` | `teqo-media` |
|
||
| URL | `https://staging.jorgesolla1313.com.br` (noindex) | `https://jorgesolla1313.com.br` |
|
||
| Build proxy | `teqo-staging-build-proxy` (127.0.0.1:5434) | `teqo-1313-build-proxy` (127.0.0.1:5433) |
|
||
| Lock | `/tmp/teqo-deploy.lock` (compartilhado — compose/workspace/registry são um só) | idem |
|
||
|
||
### Bootstrap do homeserver (uma vez, manual — fora do repo)
|
||
|
||
1. **DB:** criar `teqo_staging` no Postgres do stack
|
||
(`docker compose exec postgres createdb -U teqo teqo_staging`).
|
||
2. **Env file:** `~/stack/teqo-staging.env` (chmod 600) com `DATABASE_URL`
|
||
apontando para `teqo_staging`, `PAYLOAD_SECRET` próprio,
|
||
`NEXT_PUBLIC_SITE_URL=https://staging.jorgesolla1313.com.br`,
|
||
`REVALIDATE_SECRET` próprio e `S3_*` do bucket `teqo-media-staging`.
|
||
3. **Compose:** adicionar `teqo-staging` e `teqo-staging-migrate` ao
|
||
`~/stack/docker-compose.yml`, espelhando os serviços de produção
|
||
(`pull_policy: never`, label `org.opencontainers.image.revision` **por
|
||
serviço**, healthcheck, `extra_hosts: host-gateway`) e a porta
|
||
`127.0.0.1:1314`.
|
||
4. **Bucket:** criar `teqo-media-staging` no Garage com key própria — nunca
|
||
reutilizar a key/bucket de produção.
|
||
5. **Tunnel/DNS:** ingress `staging.jorgesolla1313.com.br` →
|
||
`http://localhost:1314` no cloudflared + DNS no Cloudflare. Opcional
|
||
(defesa em profundidade): header `X-Robots-Tag: noindex` no ingress — o
|
||
noindex versionado (robots.ts + metadata, OPS103) já cobre crawlers.
|
||
6. **GitHub Environments:** criar `staging` (sem reviewer) e `production`
|
||
(required reviewer `fsolla`) em Settings → Environments — **antes do
|
||
primeiro dispatch** (environment `production` sem reviewer é fail-open).
|
||
7. **Migrate + seed sintético (antes do primeiro dispatch):** no homeserver,
|
||
com o workspace em `~/teqo-deploy`, criar o proxy de staging manualmente
|
||
(o deploy também cria, idempotente), migrar e semear. O guard local passa
|
||
porque o alvo é `127.0.0.1` — **sem** `ALLOW_REMOTE_DB`:
|
||
|
||
```bash
|
||
ssh homeserver
|
||
cd ~/teqo-deploy && pnpm install
|
||
docker inspect teqo-staging-build-proxy >/dev/null 2>&1 || docker run -d \
|
||
--name teqo-staging-build-proxy --network stack_default \
|
||
--restart unless-stopped -p 127.0.0.1:5434:5434 \
|
||
alpine/socat TCP-LISTEN:5434,fork TCP:postgres:5432
|
||
set -a; source ~/stack/teqo-staging.env; set +a
|
||
export DATABASE_URL="${DATABASE_URL/@postgres:5432/@127.0.0.1:5434}"
|
||
pnpm migrate
|
||
pnpm db:seed:minimal
|
||
```
|
||
|
||
Nunca copiar dados de produção (PII/LGPD): o seed mínimo é sintético.
|
||
|
||
### Limites do staging (o que ele NÃO valida)
|
||
|
||
- e2e automatizado (o full continua no `verify`, que valida o commit);
|
||
- carga, caos, multi-região;
|
||
- integrações origin-bound sem configuração própria: WebAuthn/passkeys,
|
||
Google OAuth e envio de e-mail (Resend). "Staging verde" ≠ "produção
|
||
garantida" — é validação de migração/build/rollout/smoke + inspeção humana;
|
||
- dados são sintéticos; nunca PII real.
|
||
|
||
## Cutover OPS71 (passos manuais, ordem)
|
||
|
||
1. Remotes locais (config compartilhada pelos worktrees):
|
||
`git remote set-url origin git@github.com:fsolla/teqo.git` e
|
||
`git remote add forgejo ssh://git@192.168.15.142:2222/fsolla/teqo.git`
|
||
(ou `https://git.solla.dev/fsolla/teqo.git`) — o repo Forgejo congela em
|
||
main; o tracker (Issues) segue vivo por API.
|
||
2. GitHub repo: secrets `FORGEJO_API_TOKEN` + `CURSOR_API_KEY`
|
||
(Settings → Secrets and variables → Actions) e **`Allow auto-merge`**
|
||
(Settings → General → Pull Requests; ou `PATCH /repos/...` com
|
||
`allow_auto_merge: true`). Repo é público: em Settings → Actions →
|
||
General, defina fork PR workflows como não executar (ou exigir aprovação).
|
||
3. PAT local: criar token GitHub com escopo `repo` e exportar `GITHUB_TOKEN`
|
||
(para `node scripts/github-pr.mjs` e `pnpm configure:branch-protection`).
|
||
4. `GITHUB_TOKEN=… pnpm configure:branch-protection` — required check
|
||
`CI (PR) / checks` (literal de match: `checks` — o GitHub casa pelo nome do
|
||
check-run; a UI exibe workflow/job; pin PR #742), 0 reviews,
|
||
`enforce_admins: true`. **Antes do primeiro push** (senão o PR mergearia
|
||
sem required check).
|
||
5. **Desligar o runner do Forgejo** na workstation (ex.:
|
||
`systemctl --user stop forgejo-runner`) — após o primeiro PR GitHub
|
||
validado; para o schedule do `ci.yml` antigo.
|
||
6. Instalar o runner self-hosted do GitHub no homeserver (labels
|
||
`self-hosted` + `homeserver`; usuário com docker + acesso a `~/stack`;
|
||
registrar em infra-solla: `STATE.md`).
|
||
7. Push do PR do OPS71 → CI verde → auto-merge → flip da Issue no Forgejo →
|
||
deploy manual de validação.
|
||
|
||
Rollback do cutover (se o GitHub falhar cedo): religar o runner do Forgejo e
|
||
abrir PRs no Forgejo a partir do main congelado — os workflows de
|
||
`.forgejo/workflows/` foram removidos na OPS71 Fase 2, mas restauram do git
|
||
history (`git revert` do PR da remoção, ou clone do main congelado do Forgejo)
|
||
— remotes: `git remote set-url origin ssh://git@192.168.15.142:2222/fsolla/teqo.git`.
|
||
|
||
## Rollback (do deploy)
|
||
|
||
A imagem anterior **não** fica mais local após a limpeza pós-deploy (INF3/F2:
|
||
`docker builder prune` + remoção das tags locais antigas, exceto o SHA em uso)
|
||
— o registry `localhost:5000` **nunca deleta** e é a fonte do rollback. O
|
||
compose anterior fica em `~/stack/docker-compose.yml.pre-<sha-do-deploy-que-falhou>`
|
||
(um por deploy; como o staging roda antes no mesmo SHA, o backup de produção é
|
||
o compose já com a linha do staging — o restore devolve o arquivo inteiro, o
|
||
que é o correto).
|
||
|
||
```bash
|
||
ssh homeserver
|
||
cd ~/stack
|
||
# 1. apontar o compose de volta para o SHA anterior (o que estava rodando):
|
||
# 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;
|
||
# o compose referencia a tag qualificada — INF13 — e o pull basta):
|
||
docker pull localhost:5000/teqo-1313:<sha-anterior>
|
||
# Cuidado (janela de transição pós-INF13): backups gerados ANTES do fix
|
||
# usam tags bare (`image: teqo-1313:<sha>`); se o backup escolhido tiver
|
||
# tag sem prefixo, recrie o alias antes de subir:
|
||
# docker tag localhost:5000/teqo-1313:<sha-anterior> teqo-1313:<sha-anterior>
|
||
# 3. subir:
|
||
docker compose up -d teqo-1313
|
||
```
|
||
|
||
Para **staging**, o mesmo procedimento com as identidades do ambiente
|
||
(`teqo-staging`, `localhost:5000/teqo-staging:<sha-anterior>`,
|
||
`docker compose up -d teqo-staging`) — o rollback de um ambiente não toca o
|
||
container do outro (só restaura as linhas de imagem do arquivo compartilhado).
|
||
|
||
**Caveat:** uma migration de schema aplicada no passo de migrate **não** é
|
||
desfeita pelo rollback (Payload é append-only). Código velho sobre schema novo
|
||
pode se comportar mal — o caminho primário minimiza essa janela (migrate só
|
||
roda após migrator build+push+swap ok; rollout só após runner build ok). Desde
|
||
o OPS66 o migrate roda **antes** do build do runner (o build precisa do schema
|
||
novo): um build do runner que falhe depois do migrate deixa o banco à frente
|
||
do código — migrations são append-only e revisadas antes do deploy (checklist);
|
||
a correção se re-mergeia e o próximo deploy completo publica.
|
||
|
||
## Falhas conhecidas
|
||
|
||
| Sintoma | Causa | Tratamento |
|
||
| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| Prod regride para um SHA antigo após re-dispatch fora de ordem | O `flock` serializa mas não ordena; o "already deployed" não pega revision diferente | Evitar re-dispatchar SHA antigo (o requeue da OPS104 sempre despacha o head atual); se acontecer, re-dispatchar o `main` atual (rollback manual disponível na seção Rollback). Revisitar se uma regressão fora-de-ordem acontecer mesmo com o requeue |
|
||
| Job verde sem deploy ("already deployed") | O container rodando já tem a revision do SHA do job (ex.: run duplicado da mesma HEAD — `workflow_dispatch`, ou push + requeue da OPS104) | 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 |
|
||
| Merge durante aprovação pendente de produção: staging do run novo fica `pending` | O job `waiting` de approval **segura o grupo** `deploy-homeserver` (a avaliação de concurrency acontece antes do approval — comportamento do GitHub) | O run novo já nasceu (o guard não conta `waiting`); aprovar/rejeitar o run pendente libera o grupo. Nada publica sem reviewer. Revisitar se aprovações passarem a demorar sistematicamente → avaliar grupos/locks por ambiente (o lock compartilhado é decisão da OPS103) |
|
||
| Merge durante `deploy-production` `in_progress` não gera requeue | O push nasce, o `preflight` vê o deploy ativo e pula; o requeue daquele run já rodou (logo após o staging) | Nada nasce até o próximo merge ou `workflow_dispatch`. Revisitar na primeira ocorrência real → 2º requeue após produção verde, ou refinar o guard |
|
||
| Job de 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ó os deploys |
|
||
| 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 passa a referenciar a tag **qualificada** (`localhost:5000/teqo-1313:<sha>`) a partir do primeiro deploy pós-fix — até lá, produção ainda roda o compose bare (recrie o alias via `docker tag` se necessário). Recovery geral: `docker pull localhost:5000/teqo-1313:<sha>` (+ `docker tag` se o compose em uso tiver tag bare) e `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 do ambiente (prod `teqo-1313-build-proxy` em 127.0.0.1:5433; staging `teqo-staging-build-proxy` em 127.0.0.1:5434, na `stack_default`, 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 |
|
||
| `deploy-staging` falha cedo (`teqo-staging.env`/serviço ausente) | Bootstrap do staging incompleto (DB, env file, serviços no compose ou environment `staging`) | Rodar o bootstrap da seção Staging; produção nunca é tocada (staging vermelho fail-closa o job `deploy-production`) |
|
||
|
||
## Backup passivo no Forgejo (OPS76-FOLLOWUP, 2026-08-21)
|
||
|
||
O GitHub é a única fonte (main, branches, PRs, CI, tracker). O Forgejo mantém
|
||
um **backup passivo** via **pull mirror nativo**: o repo `teqo-backup` no
|
||
Forgejo (`https://git.solla.dev/fsolla/teqo-backup`) espelha o GitHub
|
||
automaticamente — **não serve push, não recebe PRs, é somente leitura**.
|
||
|
||
- **Repo:** `fsolla/teqo-backup` no Forgejo (`mirror: true`), criado via
|
||
`POST /api/v1/repos/migrate` com `clone_addr=https://github.com/fsolla/teqo.git`,
|
||
`service=github`, `mirror=true`, intervalo **8h** (`mirror_interval`).
|
||
- **Automação:** o próprio Forgejo sincroniza no intervalo (sem workflow).
|
||
Force sync manual: `POST /api/v1/repos/fsolla/teqo-backup/mirror-sync`
|
||
(o "Synchronize now" da UI).
|
||
- **Cobertura:** espelha todas as branches (54/54 verificadas) e tags; o
|
||
histórico do mirror reflete o do GitHub (que é a convergência OPS75 +
|
||
OPS76). Não espelha issues/PRs/labels (GitHub é o tracker).
|
||
- **Validação:** `git ls-remote` do `teqo-backup` deve retornar o mesmo SHA
|
||
de `refs/heads/main` do GitHub; conferir em `mirror_updated` o último sync.
|
||
- **O repo `fsolla/teqo` original do Forgejo** fica **congelado** no ponto da
|
||
convergência (OPS75) — arquivo histórico, sem mais uso.
|
||
|
||
## Segurança (decisão deliberada)
|
||
|
||
- Os jobs `deploy-staging`/`deploy-production` usam
|
||
`runs-on: [self-hosted, homeserver]` — executam como o usuário do runner no
|
||
homeserver. Estão **apenas** no `deploy.yml` (início automático por `push`
|
||
em `main` ou `workflow_dispatch`; ambos passam pelo mesmo `verify` full),
|
||
nunca no `ci-pr.yml`; o hosted (`preflight`/`verify`/`requeue`) não tem
|
||
acesso ao homeserver e o self-hosted não recebe secrets de produção (as
|
||
envs vivem no próprio homeserver, inclusive as de staging — nunca como
|
||
GitHub secrets). `main` só anda por PR mergeado com CI verde; o deploy só
|
||
roda com `verify` full verde. Fork-PRs não rodam CI (same-repo gate +
|
||
setting do repo).
|
||
- Segredos nunca ecoados: sem `set -x`, senhas via `--password-stdin` /
|
||
build-secrets; envs só no homeserver.
|
||
- Staging sem PII real (seed sintético), bucket/DB/env próprios e noindex —
|
||
nunca compartilha credencial, bucket ou dados com produção.
|
||
|
||
## C149 — Google OAuth da agenda (envs novas + GCP)
|
||
|
||
O botão **"Conectar com o Google"** da agenda usa um OAuth client do GCP. Sem
|
||
as envs, o diálogo cai no runbook antigo da service account — que segue como
|
||
fallback e não foi removida por esta entrega.
|
||
|
||
1. No GCP (mesmo projeto da service account), crie um OAuth client **Web
|
||
application** com os redirect URIs:
|
||
- `https://jorgesolla1313.com.br/campanha/agenda/google-oauth/callback`
|
||
- `http://localhost:<porta>/campanha/agenda/google-oauth/callback` (dev)
|
||
2. Publique o app em produção — **unverified** é aceito (uso interno). Em
|
||
"Testing" o refresh token expira em ~7 dias e o estado vira `erro` na UI.
|
||
3. Adicione ao `~/stack/teqo-1313.env` (homeserver, chmod 600):
|
||
`GOOGLE_CALENDAR_OAUTH_CLIENT_ID` e `GOOGLE_CALENDAR_OAUTH_CLIENT_SECRET`.
|
||
4. Deploy normal (`deploy.yml` — o merge em `main` já dispara; `workflow_dispatch`
|
||
segue como escape). O callback monta o redirect a partir de
|
||
`NEXT_PUBLIC_SITE_URL` — precisa bater exatamente com o URI registrado no
|
||
passo 1.
|
||
|
||
Desconectar no Teqo apaga o refresh token daquele lado; para revogar o acesso
|
||
de fato, revogue o app em `myaccount.google.com/permissions` (a UI orienta o
|
||
passo).
|
||
|
||
## OPS79 — última migração da plataforma antiga → nova (vertical campanha)
|
||
|
||
Operação de dados executada em 2026-08-23. Ver assistência lógica completa:
|
||
`docs/plans/ops79-ultima-migracao-dados-campanha.md` (intenção) e
|
||
`…campanha-impl.md` (impl). **A fonte (Neon, plataforma antiga) está congelada
|
||
desde o OPS80** — zero escritas desde 2026-08-23 22:30 UTC; a URL antiga segue
|
||
viva na Vercel apenas tecnicamente (desligamento de infra é o OPS81).
|
||
|
||
**Descoberta-chave da reconciliação:** a vertical campanha JÁ estava 100% íntegra
|
||
no `teo_1313` após o OPS51 (dump Neon→homeserver) — nenhum dado de operação de
|
||
campanha ficou órfão; o time de campanha não escreveu na URL antiga depois do
|
||
OPS51. O único residual real entre as plataformas era a `signature` pública
|
||
1486 (e seu contato/subscription), cujo contato originário **não existia** no
|
||
target (o id 2221 do target é OUTRA pessoa, Jorge Solla).
|
||
|
||
### Reconciliador (read-only, repetível)
|
||
|
||
Compara contagens + junta de IDs das 13 coleções da vertical e o conteúdo
|
||
semântico das join-tables de relações contra a fonte Neon.
|
||
|
||
```bash
|
||
ssh homeserver
|
||
cd ~/teqo-deploy
|
||
git fetch origin main && git checkout main && git pull --ff-only origin main
|
||
pnpm install
|
||
set -a; source ~/stack/.env; set +a # fornece NEON_DATABASE_URL
|
||
set -a; source ~/stack/teqo-1313.env; set +a # fornece DATABASE_URL (teqo_1313)
|
||
# o host `postgres` só resolve dentro da rede stack_default; do host usa-se o
|
||
# proxy socat do build (127.0.0.1:5433):
|
||
export DATABASE_URL="${DATABASE_URL/@postgres:5432/@127.0.0.1:5433}"
|
||
NEON_DATABASE_URL="$NEON_DATABASE_URL" DATABASE_URL="$DATABASE_URL" pnpm ops79:reconcile
|
||
```
|
||
|
||
Esperado: `19/19 PASS` (a `municipality_rels` reporta `+3 absorvida pós-OPS51` —
|
||
edições de portfólio na plataforma nova, não-dado). Exit `1` em qualquer nova
|
||
divergência. Nunca imprime PII.
|
||
|
||
### Migração do residual (one-off, com guard de confirmação)
|
||
|
||
Copia a `signature` 1486 + seu contato + subscription (a transação que o
|
||
`submitPetitionSignature` faz junto), reusando petition/consent já presentes e
|
||
criando o contato com **id novo** (nunca o 2221, ocupado por outra pessoa).
|
||
|
||
```bash
|
||
# dry-run (valida referentes e imprime o plano sem escrever):
|
||
NEON_DATABASE_URL="$NEON_DATABASE_URL" DATABASE_URL="$DATABASE_URL" pnpm ops79:migrate --dry-run
|
||
# aplica (ESCREVE em prod — exige a flag de intenção explícita):
|
||
OPS79_MIGRATE_CONFIRM=1 NEON_DATABASE_URL="$NEON_DATABASE_URL" DATABASE_URL="$DATABASE_URL" pnpm ops79:migrate --apply
|
||
```
|
||
|
||
Resultado registrado em 2026-08-23: contact novo **2225** (Juares Lagimar de
|
||
Souza), signature **1486** preservada (petition `fim-escala-6x1`, consent 2),
|
||
subscription **1492**; sequences ajustadas (`setval` com `true` → próximo id =
|
||
último+1). Sequências conferidas após: contact 2225 / signature 1486 /
|
||
subscription 1492. Pós-verificação: `signature` 1485=1485 (Neon×target) e
|
||
reconcile 19/19; smoke `/`, `/campanha`, `/admin` 200.
|
||
|
||
### Rollback
|
||
|
||
A migração é idempotente (fail-closed: aborta se referentes/ids divergirem). Se
|
||
precisar desfazer, DELETar as 3 linhas + ajustar sequences de volta:
|
||
`DELETE FROM signature WHERE id=1486; DELETE FROM subscription WHERE id=1492;
|
||
DELETE FROM contact_phones WHERE _parent_id=2225; DELETE FROM contact WHERE id=2225;`
|
||
e `setval('contact_id_seq',2224)`, `setval('signature_id_seq',1485)`,
|
||
`setval('subscription_id_seq',1491)`.
|
||
|
||
## OPS84 — Verificação de valores (não só entidades) da migração
|
||
|
||
Extensão do reconciliador OPS79: compara TODAS as colunas de conteúdo (não só
|
||
ids/contagens) das 13 tabelas da vertical campanha entre a fonte congelada
|
||
(Neon) e a base nova (`teqo_1313`). Foco confirmado em `vote_pledge` (votos
|
||
declarados + 3 cenários de estimativa).
|
||
|
||
**Resultado registrado em 2026-08-24:** 13/13 tabelas PASS — **0 divergências
|
||
de conteúdo**. A migração de valores está 100% íntegra. Neon e target com
|
||
dados idênticos em todas as 13 tabelas da vertical.
|
||
|
||
### Verificador de valores (read-only, repetível)
|
||
|
||
```bash
|
||
ssh homeserver
|
||
cd ~/teqo-deploy && git fetch origin main && git checkout main && git pull --ff-only origin main
|
||
pnpm install
|
||
set -a; source ~/stack/.env; set +a # fornece NEON_DATABASE_URL
|
||
set -a; source ~/stack/teqo-1313.env; set +a # fornece DATABASE_URL (teqo_1313)
|
||
export DATABASE_URL="${DATABASE_URL/@postgres:5432/@127.0.0.1:5433}"
|
||
|
||
# Todas as 13 tabelas (foco em vote_pledge):
|
||
NEON_DATABASE_URL="$NEON_DATABASE_URL" DATABASE_URL="$DATABASE_URL" \
|
||
pnpm ops84:reconcile-values
|
||
|
||
# Tabela isolada (debug/iteração):
|
||
NEON_DATABASE_URL="$NEON_DATABASE_URL" DATABASE_URL="$DATABASE_URL" \
|
||
pnpm ops84:reconcile-values --table vote_pledge
|
||
|
||
# Com relatório markdown (anexável ao issue):
|
||
NEON_DATABASE_URL="$NEON_DATABASE_URL" DATABASE_URL="$DATABASE_URL" \
|
||
pnpm ops84:reconcile-values --report /tmp/ops84-report.md
|
||
```
|
||
|
||
**Esperado:** `13/13 PASS` com 0 content diffs. Exit `1` se houver divergência
|
||
de conteúdo. Colunas classified em 3 buckets:
|
||
|
||
- **content** — falha o run (aceite: "0 divergência" ou "N listadas")
|
||
- **derived** — hook-derivadas (`declared_at/by`, `estimated_at/by`,
|
||
`created_at`, `updated_at`); informativas, não falham
|
||
- **sensitive** — PII; valores nunca impressos, só contados
|
||
|
||
Colunas jsonb comparadas via `col::text` (preserva `1` vs `1.0` como
|
||
divergência; SQL NULL ≠ JSON null). Zero normalização (trim/round/coalesce).
|
||
|
||
### Fallback: Neon inacessível (OPS81 já executado)
|
||
|
||
Se o Neon estiver inacessível (OPS81 desligou a infra), usar o dump do OPS51
|
||
como base de comparação:
|
||
|
||
```bash
|
||
# Restaurar dump em scratch DB local:
|
||
createdb teqo_1313_compare_src
|
||
pg_restore -d teqo_1313_compare_src \
|
||
/srv/hdd/backups/teqo-neon-pre-migracao/teqo-neon-full-20260817-204800.dump
|
||
|
||
# Apontar NEON_DATABASE_URL para o scratch:
|
||
NEON_DATABASE_URL="postgresql://teqo:teqo@localhost:5432/teqo_1313_compare_src" \
|
||
DATABASE_URL="$DATABASE_URL" \
|
||
pnpm ops84:reconcile-values --report /tmp/ops84-report-dump.md
|
||
```
|
||
|
||
**Nota:** dump é pré-freeze (2026-08-17) — deltas legítimos entre 08-17 e
|
||
08-23 (quando o Neon foi congelado) aparecem como divergências derivadas. O
|
||
relatório declara qual base foi usada.
|
||
|
||
### Rollback
|
||
|
||
Não há escrita; "rollback" = rodar de novo (idempotente, read-only).
|
||
|
||
## C155 — backfill do acervo de falas em produção
|
||
|
||
Operação de dados do catálogo de falas (`speech`/`speechSegment`, C153/C154):
|
||
processa as 54ª–57ª legislaturas (~1.011 discursos) contra o `teqo_1313` e
|
||
imprime um relatório por legislatura + a cobertura do banco. O import é
|
||
idempotente por `sourceKey` (reexecutar atualiza metadados e pula VOD/ASR do
|
||
que já tem segmentos), então o run é resumível — falha de rede em uma
|
||
legislatura não derruba as outras.
|
||
|
||
**Pré-requisitos:** o deploy que aplicou as migrations do catálogo
|
||
(`20260913_001112_add_speech_catalog`, `20260913_001200_add_speech_segment_trgm_index`,
|
||
`20260913_145820_add_speech_search_text`, `20260913_150000_backfill_speech_search_text_trgm_index`)
|
||
já está em produção desde o deploy de `424ee311` (2026-09-13); `DEEPINFRA_API_KEY`
|
||
no `~/stack/.env` do homeserver; `~/teqo-backfill` no SHA desejado com
|
||
`pnpm install`; espaço em `/srv/hdd/backups/teqo-camara` para o cache de MP4
|
||
(o cache do C153 foi descartado com o worktree; o backfill baixa de novo).
|
||
|
||
```bash
|
||
ssh homeserver
|
||
source ~/.nvm/nvm.sh # Node 24 (engines do repo)
|
||
cd ~/teqo-backfill && git fetch origin && git checkout <SHA> && pnpm install
|
||
set -a; source ~/stack/.env; set +a # DEEPINFRA_API_KEY
|
||
set -a; source ~/stack/teqo-1313.env; set +a # DATABASE_URL + NODE_ENV=production
|
||
# o host `postgres` só resolve na rede do stack; do host usa-se o proxy socat:
|
||
export DATABASE_URL="${DATABASE_URL/@postgres:5432/@127.0.0.1:5433}"
|
||
export CAMARA_IMPORT_CONFIRM=1 # guard C155: escrita em produção
|
||
|
||
# smoke (4 discursos, ~2 min; repetir para provar idempotência: 0 criados/0 ASR):
|
||
pnpm camara:import --all --limit 1 --out /srv/hdd/backups/teqo-camara
|
||
# run completo (tmux; ~10–15h de parede, resumível):
|
||
tmux new -d -s c155 "pnpm camara:import --all --out /srv/hdd/backups/teqo-camara 2>&1 | tee -a /srv/hdd/backups/c155-run.log"
|
||
# validação pós-run (read-only):
|
||
pnpm camara:import --coverage --out /srv/hdd/backups/teqo-camara
|
||
pnpm camara:import --verify-links 3 --out /srv/hdd/backups/teqo-camara
|
||
```
|
||
|
||
O guard `CAMARA_IMPORT_CONFIRM=1` é exigido sempre que a escrita não é
|
||
provadamente local (`NODE_ENV=production`, host remoto ou `ALLOW_REMOTE_DB`);
|
||
`--coverage` e `--verify-links` são read-only e não exigem a flag. A escrita é
|
||
confinada a `speech`/`speechSegment`/rels; nenhum objeto de mídia vai para o
|
||
S3 (os MP4 ficam só no cache local). Crédito CC BY 4.0 da Câmara mantido no
|
||
admin/relatórios.
|
||
|
||
**Resultado registrado (2026-09-13/14):** **997 discursos** em produção
|
||
(54ª=1, 55ª=348, 56ª=414, 57ª=234) — 947 com trecho, 924 com vídeo, 920 com
|
||
segmentos; ASR 925 chamadas / 45,4h / ~US$1,23; LLM ~US$0,11; ~11h de máquina
|
||
(9h de runs + ~2h de retry por data). A 55ª veio pelo fallback de listagem
|
||
por ano/mês (a API responde 500 em **outubro/2018**; ~14 discursos ficaram
|
||
fora — reexecutar `pnpm camara:import --date <dia>` quando a fonte voltar).
|
||
O smoke idempotente (`--all --limit 1` duas vezes) provou 0 criados/0 ASR/0 LLM
|
||
na segunda passada; `--verify-links 3` deu 17/18 ok (1 warning transitório de
|
||
CDN). Relatórios completos em `/srv/hdd/backups/teqo-camara/reports/` e no
|
||
changelog `docs/changelog/2026-09-13-c155.md`.
|
||
|
||
### Rollback
|
||
|
||
O import é aditivo e idempotente; não há migração para desfazer. Para remover
|
||
uma legislatura (ex.: reimportar do zero), apagar os discursos e seus
|
||
segmentos — as rels (`speech_rels`, `speech_topics`, `speech_scopes`) caem por
|
||
cascade:
|
||
|
||
```sql
|
||
DELETE FROM "speech_segment" WHERE "speech_id" IN (SELECT id FROM "speech" WHERE "legislature" = '55');
|
||
DELETE FROM "speech" WHERE "legislature" = '55';
|
||
```
|
||
|
||
Para desfazer o acervo inteiro: `DELETE FROM "speech_segment"; DELETE FROM "speech";`
|
||
(a cobertura `--coverage` volta a zero). Reexecutar `--all` reconstrói.
|
||
|
||
## Referências
|
||
|
||
- `scripts/deploy-homeserver.sh` — o script (fonte da verdade do fluxo; parametrizado por `TEQO_ENV`)
|
||
- `.github/workflows/deploy.yml` — preflight → verify → deploy-staging → requeue/deploy-production (OPS103/OPS104)
|
||
- `docs/plans/ops53-ci-deploy-homeserver*.md` — intenção e decisões (era Forgejo)
|
||
- `docs/plans/ops71-ci-github-actions-tracker-forgejo*.md` — o cutover
|
||
- `docs/plans/ops103-staging-homeserver*.md` — staging com verify único e aprovações separadas
|
||
- `docs/plans/ops104-disparo-automatico-do-deploy*.md` — início automático + requeue (staging automático, produção com review)
|
||
- infra-solla: `STATE.md`, `plano-infra-final.md` §"Arquitetura de deploy"
|