ops(OPS53): etapa de deploy no CI para o homeserver (teqo-1313)

Job deploy em ci.yml (runs-on host na workstation) streama
scripts/deploy-homeserver.sh para o homeserver via SSH: build dos stages
runner+migrator com secrets do teqo-1313.env, push no registry local,
migrate pelo serviço de maintenance antes do rollout, up + smoke. Zero
secrets novos no Forgejo; HEAD guard + flock; rollback automático com
backup do compose; runbook docs/ops/teqo-1313-deploy.md; invariantes
unit do script (bash -n + estrutura).
This commit is contained in:
Francisco Solla 2026-08-17 21:24:32 -03:00 committed by fsolla
parent 87410ccc63
commit 6a3f61f968
9 changed files with 451 additions and 14 deletions

View file

@ -1,6 +1,9 @@
# Post-merge verifier on `main`: always the FULL suite (no affected skips).
# No deploy step: production is self-hosted on the homeserver
# (jorgesolla1313.com.br), deployed outside this pipeline.
# When the suite is green, the `deploy` job publishes teqo-1313 on the
# homeserver (OPS53): it streams scripts/deploy-homeserver.sh to the homeserver
# over SSH; the remote script builds the standalone image at the pushed SHA,
# pushes it to the local registry (localhost:5000), applies pending migrations
# through the compose maintenance service and rolls the container out.
#
# Forgejo port (OPS50): Postgres services publish to per-job HOST ports on the
# runner machine (act/forgejo runner semantics — the canonical 5432 is the
@ -204,3 +207,22 @@ jobs:
to_entries
| all(.value.result == "success" or .value.result == "skipped")
'
deploy:
name: deploy homeserver (teqo-1313)
# OPS53. Runs directly on the runner HOST (not a job container): the
# deploy needs the workstation's SSH key to reach the homeserver
# (`ssh homeserver`) and streams the whole remote build/migrate/rollout
# to this job's logs. Deliberately wired ONLY here — push to main +
# workflow_dispatch — never in ci-pr.yml: a host job is code execution as
# `fsolla` on the workstation, and main only ever moves through a merged
# PR whose full suite was already green.
needs: [checks]
runs-on: host
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- name: Deploy teqo-1313 on the homeserver
run: |
set -euo pipefail
ssh homeserver "bash -s -- $GITHUB_SHA" < scripts/deploy-homeserver.sh

View file

@ -12,7 +12,7 @@ These are product/architecture constraints — not a ban on editing existing cod
- Single Next.js app, three route groups: `(frontend)` public site, `(payload)` admin, `(campaign)` internal campaign tool. No separate Rust service or frontend app for `/campanha`; it is modeled with Payload collections. The campaign roles are `coordinator` (label "Coordenador Geral"), `advisor` ("Assessor"), `leader` ("Liderança"), and `candidate` ("Candidato", unrestricted visibility), all on the separate auth collection `campaignUser` (see "Campaign auth" below). An advisor sees and manages ONLY the municipalities they administer; a leader is locked down to the supporter contact tool.
- **2026-07-23 remodel (DEPLOYED to production 2026-07-23): "Município" is the operational unit** (it replaced "Praça", which replaced "Núcleo Eleitoral"). Municípios are PREDEFINED (seeded, geography read-only): 435 rows — one per Bahia municipality (416), except Salvador (19 zone Municípios, ZE 1–19); Camaçari is a single whole municipality (ZE 170/171 aggregated); shared/leaky TSE zones are cut at the municipal boundary. Code identifier: `municipality`. Leaderships are one record per person (`contact` UNIQUE) linked to N municipalities and N organizations; vote pledges are per leadership×municipality with `declaredVotes` (staff-entered, leader-visible) vs `estimatedVotes` (three scenarios, staff-only, hidden from the leader). Campaign production data was reset by the remodel migrations (public-site data untouched). Master plan: `docs/plans/remodelagem-municipios.md` (supersedes `remodelagem-pracas.md`). Details: "Campaign Municípios model" below.
- Production is self-hosted on the homeserver (`jorgesolla1313.com.br`, behind the Cloudflare tunnel; container/database `teqo-1313`/`teqo_1313`). **There is no Vercel deploy anymore** — the Vercel deploy job, `vercel-promote.yml`, the Vercel scripts/`vercel.json` and the `VERCEL_*` repo secrets were removed in OPS50 (2026-08-17). The CI (`ci.yml` on `main`) is a pure verifier: full suite green, no deploy step. Deploying the homeserver container is an ops runbook outside this repo (infra-solla). **The production database lives on the homeserver Postgres (`teqo_1313`, migrated from Neon in OPS51, 2026-08-17)** — the full production content (contacts, signatures, subscriptions, campaign) was restored there via `pg_dump`/`pg_restore` (dump in `/srv/hdd/backups/teqo-neon-pre-migracao/`); Neon remains alive and untouched as the documented rollback (re-point `DATABASE_URL` in `~/stack/teqo-1313.env`). Media still on Vercel Blob until OPS52 (covers render broken pre-existing — 40 media rows reference local `/api/media/file/...` paths with no files anywhere; OPS52 scope). Never point dev/test databases at the homeserver prod DB either — the local guards (`guard-dev-db`, `assertTestDatabase`) still enforce `localhost`/`_test`-suffixed targets.
- Production is self-hosted on the homeserver (`jorgesolla1313.com.br`, behind the Cloudflare tunnel; container/database `teqo-1313`/`teqo_1313`). **There is no Vercel deploy anymore** — the Vercel deploy job, `vercel-promote.yml`, the Vercel scripts/`vercel.json` and the `VERCEL_*` repo secrets were removed in OPS50 (2026-08-17). The CI (`ci.yml` on `main`) is the verifier — full suite green — **plus the deploy step (OPS53, 2026-08-17)**: the `deploy` job (runs-on `host` on the workstation runner) streams `scripts/deploy-homeserver.sh` to the homeserver over SSH; the remote script builds the standalone image at the pushed SHA (BuildKit secrets from `~/stack/teqo-1313.env`), pushes it to the local registry (`localhost:5000`), applies pending migrations through the compose maintenance service `teqo-1313-migrate` **before** the rollout, and recreates the container. Merge em `main` verde == site atualizado. Runbook (rollback, falhas conhecidas): `docs/ops/teqo-1313-deploy.md`. **The production database lives on the homeserver Postgres (`teqo_1313`, migrated from Neon in OPS51, 2026-08-17)** — the full production content (contacts, signatures, subscriptions, campaign) was restored there via `pg_dump`/`pg_restore` (dump in `/srv/hdd/backups/teqo-neon-pre-migracao/`); Neon remains alive and untouched as the documented rollback (re-point `DATABASE_URL` in `~/stack/teqo-1313.env`). Media still on Vercel Blob until OPS52 (covers render broken pre-existing — 40 media rows reference local `/api/media/file/...` paths with no files anywhere; OPS52 scope). Never point dev/test databases at the homeserver prod DB either — the local guards (`guard-dev-db`, `assertTestDatabase`) still enforce `localhost`/`_test`-suffixed targets.
- Donations are NOT handled in this app — they run through QueroApoiar (`apoiar.me/jorgesolla`, TSE-homologated). The site only needs a "Doar" link/CTA out to that URL.
- **Embasamento de produto/domínio:** `docs/research/` guarda o compêndio de literatura (campanha de DF na Bahia, geografia do voto, cartografia eleitoral) e o relatório de discovery aprovado (2026-07-21) com o playbook dado→decisão. Para métricas, análises territoriais, mapa e priorização em `/campanha`, consulte-o antes de inventar — o kernel: a disputa de DF é conta de quociente fragmentada; leitura útil é relativa e local (captura, LQ, quantis, cobertura de meta por pledges), nunca % estadual absoluto.

View file

@ -7,7 +7,7 @@ Uma página. Norte: simplicidade (só GitHub + Cursor), agente entrega em `main`
| Ambiente | Conteúdo | Quem usa |
| ------------------------------------------- | ------------------------------------------------------------ | -------------------------------- |
| **Mínimo** (`teqo_test` local / service CI) | schema migrado + `pnpm db:seed:minimal` (sintético, sem PII) | agentes, Cursor Cloud, CI |
| **Prod** (Neon prod) | real | deploy Vercel gated por `ci.yml` |
| **Prod** (`teqo_1313`, homeserver) | real | job `deploy` do `ci.yml` (OPS53) |
Agentes **nunca** recebem `DATABASE_URL` de prod e nunca setam `ALLOW_REMOTE_DB`. Não há mais smoke Neon `stage` / `ALLOW_STAGE_TEST_DB` / `ci-stage.yml`.
@ -15,7 +15,7 @@ Agentes **nunca** recebem `DATABASE_URL` de prod e nunca setam `ALLOW_REMOTE_DB`
```text
claim → feature branch → PR --base main → CI PR green (cascade + skips) → auto-merge em main
main → ci.yml full suite (verificador puro; sem deploy — produção é self-hosted no homeserver, runbook fora do repo)
main → ci.yml full suite verde → job `deploy` (homeserver, OPS53 — merge em main == site atualizado)
```
**Skills:** `plan-issue` (intenção + Issues) → `work-issue` (humano: Issue já claimada → impl plan → confirmação → execução) ou `agent-work-issue` (pool: já claimada → impl plan → execução sem pausa) → `/simplify` → `capture-review-debts` → PR `--base main` → `project-status`. `docs/roadmap.md` = legado congelado; fonte canônica = GitHub Issues.
@ -69,14 +69,14 @@ Arquivos de "lista" que N agentes escrevem em paralelo (changelog, plans, migra
## CI por alvo
| Workflow | Trigger | Banco | Passos |
| ------------------------------------ | -------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `ci-pr.yml` | PR → `main` | service Postgres 17 | Fase 1 barata → fase 2 cara; skips via `ci-scope.mjs`; fail-fast cancela o run no 1º job vermelho; rollup `checks` |
| `ci.yml` | push / dispatch `main` | service Postgres | Full sempre; fail-fast cancela o run no 1º job vermelho; rollup `checks`; sem deploy (produção self-hosted no homeserver) |
| `issue-done-on-main-merge.yml` | PR merged → `main` | — | `Closes`/`Fixes` → `done` + `in-prod` |
| `plan-issue-ready-on-main-merge.yml` | PR merged → `main` | — | `Related #N` aguardando plano → `ready` (OPS18; soft-skip) |
| `agent-pr-ready-automerge.yml` | PR `cursor/*` → `main` (open/sync/…) | — | Draft→Ready + `gh pr merge --auto --rebase` (safety net; audit incluso) |
| `agent-pool.yml` | schedule / PR closed `main` / dispatch | — | Supervisor do pool (`POOL_GITHUB_TOKEN`) |
| Workflow | Trigger | Banco | Passos |
| ------------------------------------ | -------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ci-pr.yml` | PR → `main` | service Postgres 17 | Fase 1 barata → fase 2 cara; skips via `ci-scope.mjs`; fail-fast cancela o run no 1º job vermelho; rollup `checks` |
| `ci.yml` | push / dispatch `main` | service Postgres | Full sempre; fail-fast cancela o run no 1º job vermelho; rollup `checks`; job `deploy` (runs-on `host`) publica no homeserver via `scripts/deploy-homeserver.sh` (OPS53) |
| `issue-done-on-main-merge.yml` | PR merged → `main` | — | `Closes`/`Fixes` → `done` + `in-prod` |
| `plan-issue-ready-on-main-merge.yml` | PR merged → `main` | — | `Related #N` aguardando plano → `ready` (OPS18; soft-skip) |
| `agent-pr-ready-automerge.yml` | PR `cursor/*` → `main` (open/sync/…) | — | Draft→Ready + `gh pr merge --auto --rebase` (safety net; audit incluso) |
| `agent-pool.yml` | schedule / PR closed `main` / dispatch | — | Supervisor do pool (`POOL_GITHUB_TOKEN`) |
Action runtimes: `actions/checkout@v5`, `actions/setup-node@v5`, `pnpm/action-setup@v6` e `styfle/cancel-workflow-action@0.13.1` usam Node 24 nativo — sem `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24`.
@ -90,7 +90,7 @@ Fast gate: `pnpm gate:fast`. Push: `pnpm push`.
| `CURSOR_API_KEY` | Supervisor do pool (Cursor Cloud) |
| `POOL_GITHUB_TOKEN` | PAT `actions:write` + `issues:write` (variables do pool; `GITHUB_TOKEN` costuma 403) |
Sem deploy Vercel: o job `deploy`, `vercel-promote.yml`, os scripts de Vercel e `vercel.json` foram removidos no OPS50 — produção é self-hosted no homeserver (`jorgesolla1313.com.br`), deploy via runbook de infra fora deste repo.
Deploy (OPS53): o job `deploy` do `ci.yml` roda no host do runner (workstation) e streama `scripts/deploy-homeserver.sh` para o homeserver via SSH — build no homeserver (secrets de `~/stack/teqo-1313.env`), registry `localhost:5000`, migrate pelo serviço de maintenance `teqo-1313-migrate` antes do rollout, smoke pós-deploy. Nenhum secret novo no Forgejo. Runbook (rollback, falhas conhecidas): `docs/ops/teqo-1313-deploy.md`. O deploy Vercel (`vercel-promote.yml`, scripts, `vercel.json`) foi removido no OPS50.
## Cursor Cloud

View file

@ -6,6 +6,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
**Recently resolved (2026-08-17):** **OPS51** — o banco de produção saiu do Neon para o Postgres do homeserver (`teqo_1313`, container `teqo-1313`), sem mudança de config: o `~/stack/teqo-1313.env` já apontava para o banco preparado (criado 16/08 com role própria + cadeia completa de 56 migrations + zero conteúdo — verificado), então restaurar = a produção "apareceu" no container. **Divergência deliberada da intenção (questão A/B validada como B):** rejeitado criar `teqo_prod` novo — o argumento "o banco do 1313 tem vida própria" não se sustenta (o site 1313 É a produção deste repo) e o OPS53 já trata `teqo_1313` como produção. Execução 100% no homeserver (PII nunca tocou a workstation): URL do Neon puxada do Vercel (`vercel env pull`, projeto `jorgesolla`, `DATABASE_URL_UNPOOLED` → `NEON_DATABASE_URL` em `~/stack/.env` chmod 600), `pg_dump -Fc` (Neon PG 17.10) → `/srv/hdd/backups/teqo-neon-pre-migracao/teqo-neon-full-20260817-204800.dump` (5,8 MB, sha256 `f12196f2…`), `pg_restore --no-owner --no-privileges` como a role `teqo_1313` (drop schema + grant, padrão do `db-pull`), verificado: contagens por tabela idênticas ao Neon (1919 contatos, 39 posts, 1487 assinaturas, 1484 subscrições, 35 campaign_users, 392 lideranças, 2 pledges, 435 municípios, 6 atividades; supporter segue 0 — blocker LGPD ainda de pé), sequences no topo (`last_value == max(id)`), objetos com dono `teqo_1313`, `payload migrate` no-op (56 = 56, serviço de maintenance `teqo-1313-migrate`), 3 credenciais WebAuthn + 34 sessions + 5 chaves Consent restauradas (RP ID não mudou — biometria segue valendo). Smoke sem deploy: `/`, artigo com conteúdo restaurado, `/campanha/login`, `/admin`, `POST /api/revalidate` (`posts` + `global_privacy-policy`), WebAuthn `login-options`, barreira 307 sem sessão — verdes; smoke de browser (login, biometria, convites) validado manualmente. **Rollback documentado:** Neon nunca foi escrito (só lido); voltar = re-apontar `DATABASE_URL` do `teqo-1313.env` para o Neon + `compose up -d`. **Débito registrado (→ OPS52):** as 40 rows de `media` têm URLs relativas (`/api/media/file/…`) sem arquivos em lugar nenhum — capas dão 500 no homeserver e **404 no próprio Vercel/produção atual** (estado pré-existente; migração manteve paridade). Repo: plano da intenção concluído, `AGENTS.md` e help de `db:pull`/`check-push-chain` sem a referência ao Neon; infra-solla `STATE.md` atualizado à parte.
**Recently resolved (2026-08-17):** **OPS53** — o deploy do site 1313 virou etapa do CI: mergir em `main` com a suite verde == site atualizado em `jorgesolla1313.com.br`, sem passo manual. O job `deploy` em `.forgejo/workflows/ci.yml` roda no host do runner (workstation, `runs-on: host`) e streama `scripts/deploy-homeserver.sh` para o homeserver via SSH (`ssh homeserver "bash -s -- <sha>"`); o script remoto (bash puro, homeserver sem dependências novas) faz: guard de HEAD (`git ls-remote` de `main` == SHA do job, senão skip "stale"), `flock` de serialização, workspace `~/teqo-deploy` (clone do Forgejo local), `docker login localhost:5000` (REGISTRY_USER/PASSWORD de `~/stack/.env`), build dos stages `runner`+`migrator` no homeserver (`--network stack_default` para a geração estática alcançar o `postgres:5432` do compose; secrets de `~/stack/teqo-1313.env` via BuildKit, sem camadas/logs), push no registry local, swap dos tags de imagem no compose (com backup `docker-compose.yml.pre-<sha>`), **migrate pelo serviço de maintenance `teqo-1313-migrate` antes do rollout**, `docker compose up -d`, espera do healthcheck e smoke (`/`, `/campanha/login`, `/admin`, barreira 307, WebAuthn `login-options` com body JSON, `POST /api/revalidate` com o secret real — rehearsal validado contra o site vivo). Falha pós-swap → rollback automático best-effort (restore do backup + `up -d` da imagem anterior) + job vermelho. **Divergência deliberada da intenção (fato novo):** a intenção assumia o runner no homeserver; verificado (`infra-solla/STATE.md` + ao vivo) que o runner é a `workstation-runner` na workstation e o stack de produção vive no homeserver — a abordagem A foi corrigida para "job `runs-on: host` na workstation + todo o deploy no homeserver via SSH", o que manteve **zero secrets novos no Forgejo** e zero mudança de infra no compose. Nenhum secret no repo; envs seguem chmod-600 no homeserver. Runbook (rollback manual, falhas conhecidas, decisão de segurança do host label): `docs/ops/teqo-1313-deploy.md` (novo). Guarda estrutural nova: `tests/unit/deployScript.unit.spec.ts` (bash -n + invariantes do pipeline: HEAD guard, BuildKit secrets, ordem migrate→rollout, smoke). AGENTS.md/AGENT-OPS.md atualizados. Primeiro deploy real = o merge deste próprio item (tracer bullet); infra-solla `STATE.md` anotado à parte.
**Recently resolved (2026-08-16):** **CAMPAIGN-HOME-RESPONSIVE**: a primeira dobra da home pública agora acompanha as referências do Penpot em 393, 768, 1024, 1280, 1366 e 1920 px. Retrato, aliados, marca, texto, CTAs e prova parlamentar interpolam entre os tamanhos com `clamp()`, `vw` e `dvh`, sem saltos por proporção de tela nem overflow horizontal; texto, ações e condecorações permanecem no fluxo e crescem com os ajustes de espaçamento da WCAG 1.4.12. No desktop, a escala e a posição horizontal das fotos dependem somente da largura: Solla, Lula e Wagner ficam ancorados ao rodapé real do hero até quando o conteúdo aumenta a seção, e um palco centralizado de 1920 px preserva a proximidade entre retrato e texto em monitores ultrawide. O E2E fixa a geometria das seis referências, larguras intermediárias, transições de proporção, desktops baixos, ultrawide e os overrides de acessibilidade; o caso de regressão também foi executado no Firefox. Sem migration, schema ou mudança de conteúdo.
**Recently resolved (2026-08-16):** **OPS50** (parte 1): CI e scripts de agentes portados do GitHub para o Forgejo (`git.solla.dev`, tracker já migrado nas Issues #1–#3), sem nenhuma dependência do GitHub Actions. Os 10 workflows de `.github/workflows/` viraram 8 nativos em `.forgejo/workflows/` (`ci.yml` com deploy gated + requeue por `forgejo-dispatch.mjs`, `ci-pr.yml` com cascade de jobs e checks agregador, `agent-pool.yml`, `agent-pr-ready-automerge.yml`, `issue-done-on-main-merge.yml`, `plan-issue-ready-on-main-merge.yml`, `vercel-promote.yml`, `archive-cursor-agent.yml`) e `.github/workflows/*` + `.github/actions/` foram removidos (cancel-workflow-on-failure não existe no Forgejo). Novo runner: Forgejo runner v13 na workstation (capacity 4), jobs paralelos em serviços Postgres isolados por porta (int 5433, build 5434, e2e shards 5435/5436) com `DB_HOST` calculado do gateway do bridge Docker (byte order corrigida via `strtonum`). Libs de agente: `agent-github.mjs`/`agent-pool-github.mjs` substituídas por `agent-forgejo.mjs` + `agent-pool-forgejo.mjs` sobre o novo cliente zero-dependência `forgejo-api.mjs` (token `FORGEJO_API_TOKEN`/`GITHUB_TOKEN`, base URL `FORGEJO_API_URL`/`GITHUB_SERVER_URL`/default, shapes normalizados ao contrato gh-flavored; novos `isDraft`/`markPullRequestReady`); helpers `forgejo-dispatch.mjs`, `forgejo-pr-automerge.mjs` (poll de checks + ready de draft), `forgejo-issue-transition.mjs`; auto-merge do PR de planos já validado em produção (PR #4). Bug de port corrigido em `pnpm issue` (await em função sync) e validado ao vivo contra o Forgejo. Testes: `forgejoApi.unit.spec.ts` novo (10 testes), pin do matrix do `ci.yml` movido para `.forgejo/workflows/`; tsc/lint/prettier/knip/check:cycles/2283 unit verdes. Pendente (parte 2): migrar as Issues abertas do GitHub, reescrever `.agents/`, `AGENTS.md`, `docs/AGENT-OPS.md` e `docs/roadmap.md`, e passos humanos (secrets no Forgejo, `pnpm configure:branch-protection`, arquivar o repo GitHub).

View file

@ -0,0 +1 @@
**Recently resolved (2026-08-17):** **OPS53** — o deploy do site 1313 virou etapa do CI: mergir em `main` com a suite verde == site atualizado em `jorgesolla1313.com.br`, sem passo manual. O job `deploy` em `.forgejo/workflows/ci.yml` roda no host do runner (workstation, `runs-on: host`) e streama `scripts/deploy-homeserver.sh` para o homeserver via SSH (`ssh homeserver "bash -s -- <sha>"`); o script remoto (bash puro, homeserver sem dependências novas) faz: guard de HEAD (`git ls-remote` de `main` == SHA do job, senão skip "stale"), `flock` de serialização, workspace `~/teqo-deploy` (clone do Forgejo local), `docker login localhost:5000` (REGISTRY_USER/PASSWORD de `~/stack/.env`), build dos stages `runner`+`migrator` no homeserver (`--network stack_default` para a geração estática alcançar o `postgres:5432` do compose; secrets de `~/stack/teqo-1313.env` via BuildKit, sem camadas/logs), push no registry local, swap dos tags de imagem no compose (com backup `docker-compose.yml.pre-<sha>`), **migrate pelo serviço de maintenance `teqo-1313-migrate` antes do rollout**, `docker compose up -d`, espera do healthcheck e smoke (`/`, `/campanha/login`, `/admin`, barreira 307, WebAuthn `login-options` com body JSON, `POST /api/revalidate` com o secret real — rehearsal validado contra o site vivo). Falha pós-swap → rollback automático best-effort (restore do backup + `up -d` da imagem anterior) + job vermelho. **Divergência deliberada da intenção (fato novo):** a intenção assumia o runner no homeserver; verificado (`infra-solla/STATE.md` + ao vivo) que o runner é a `workstation-runner` na workstation e o stack de produção vive no homeserver — a abordagem A foi corrigida para "job `runs-on: host` na workstation + todo o deploy no homeserver via SSH", o que manteve **zero secrets novos no Forgejo** e zero mudança de infra no compose. Nenhum secret no repo; envs seguem chmod-600 no homeserver. Runbook (rollback manual, falhas conhecidas, decisão de segurança do host label): `docs/ops/teqo-1313-deploy.md` (novo). Guarda estrutural nova: `tests/unit/deployScript.unit.spec.ts` (bash -n + invariantes do pipeline: HEAD guard, BuildKit secrets, ordem migrate→rollout, smoke). AGENTS.md/AGENT-OPS.md atualizados. Primeiro deploy real = o merge deste próprio item (tracer bullet); infra-solla `STATE.md` anotado à parte.

View file

@ -0,0 +1,83 @@
# Runbook: deploy do site 1313 no homeserver (OPS53)
O deploy de produção é uma etapa do CI (`ci.yml` no Forgejo): mergir em `main`
com a suite verde publica o site em `jorgesolla1313.com.br` — sem passo manual.
## Gatilho e fluxo
1. `git push` para `main` (via PR merge) — ou `workflow_dispatch` no `ci.yml`
para re-deployar a HEAD de `main` manualmente.
2. Suite completa verde (static/int/build/e2e/checks).
3. Job `deploy` (runs-on `host`, na **workstation**) roda
`ssh homeserver "bash -s -- <sha>" < scripts/deploy-homeserver.sh`.
4. O script, no **homeserver**: guarda de HEAD (`main` remoto == SHA do job,
senão skip) → `flock` (serializa) → build da imagem standalone + migrator
(BuildKit, secrets do `~/stack/teqo-1313.env`, rede `stack_default`) →
push em `localhost:5000` → swap dos tags de imagem no
`~/stack/docker-compose.yml` (backup antes) → **migrations**
(`docker compose --profile maintenance run --rm teqo-1313-migrate`) →
`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 no commit; nada é publicado pela metade.
## Onde roda cada coisa
| Máquina | Papel |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **workstation** (16c/61GB) | runner Forgejo (`workstation-runner`); job `deploy` roda no host (chave SSH `~/.ssh/id_ed25519` → `ssh 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; workspace `~/teqo-deploy` (clone do Forgejo local) |
Segredos **não** ficam no Forgejo: o script sourceia `~/stack/teqo-1313.env` e
`~/stack/.env` (chmod 600) no próprio homeserver. Nenhum secret novo em
Settings → Actions.
## Rollback
A imagem anterior continua local e no registry (`localhost:5000/teqo-1313:<sha>`
— o registry nunca deleta). O compose anterior fica em
`~/stack/docker-compose.yml.pre-<sha-do-deploy-que-falhou>`.
```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. a imagem antiga ainda está local; se não, puxe do registry:
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
```
**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 build+push ok; rollout só após migrate ok).
## Falhas conhecidas
| Sintoma | Causa | Tratamento |
| ---------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Job verde sem deploy ("stale run") | Push mais novo venceu; o deploy antigo saiu da fila | Nada a fazer — o run do SHA novo deploya |
| 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 |
| `checkout@v5` falha no job `host` | Label host do act_runner | Fallback: baixar o script direto do Forgejo (`curl -s https://git.solla.dev/fsolla/teqo/raw/branch/main/scripts/deploy-homeserver.sh`) |
| 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 |
## Segurança (decisão deliberada)
- O job `deploy` usa `runs-on: host` — executa como `fsolla` na workstation.
Está **apenas** no `ci.yml` (push em `main` + dispatch), nunca no `ci-pr.yml`.
`main` só anda por PR mergeado com suite verde. Fork-PRs não disparam
workflows no Forgejo (default off — verificado).
- Segredos nunca ecoados: sem `set -x`, senhas via `--password-stdin` /
build-secrets; envs só no homeserver.
## Referências
- `scripts/deploy-homeserver.sh` — o script remoto (fonte da verdade do fluxo)
- `docs/plans/ops53-ci-deploy-homeserver*.md` — intenção e decisões
- infra-solla: `STATE.md`, `plano-infra-final.md` §"Arquitetura de deploy"

View file

@ -0,0 +1,95 @@
# Impl: Etapa de deploy no CI para o homeserver (container `teqo-1313`)
Status: aprovado
Atualizado em: 2026-08-17
Issue: #8
Intenção: docs/plans/ops53-ci-deploy-homeserver.md
Appetite restante: herdado (~1–2 dias eng)
## Leitura da intenção
- **Outcome:** `git push` em `main` com CI verde ⇒ site 1313 atualizado (container novo com o SHA do commit), sem passo manual; migrations aplicadas contra o `teqo_1313` **antes** do rollout (ordem migrate → rollout); rollback documentado; falha de deploy deixa o commit vermelho e nada meio-publicado.
- **O que NÃO negociar:** build nunca no laptop do dev; ordem migrate → rollout; nada de schema migration contra produção fora do passo de migrate; imagem só publicada com full suite verde; rollback executável.
- **O que reavaliar (fato novo crítico):** a intenção assume "o runner Forgejo roda no próprio homeserver (tem Docker + registry + compose)". **Não é verdade** (verificado em `infra-solla/STATE.md` e ao vivo): o runner é `workstation-runner` (act_runner v0.2.11) na **workstation** (16c/61GB, Pop!\_OS); o homeserver (8c/16GB, laptop) é que roda o stack inteiro (`~/stack/docker-compose.yml`: postgres com `teqo_1313`, forgejo, registry :5000 com htpasswd, cloudflared, `teqo-1313`). `ssh homeserver` (tailnet) funciona da workstation (chave existente, verificada). A arquitetura do `plano-infra-final.md` §"Arquitetura de deploy" (runner → build → registry :5000 → migrate + pull + up no homeserver) permanece o norte — só que o "runner" fica na workstation e a orquestração do deploy cruza tailnet por SSH.
## Abordagem recomendada
```mermaid
flowchart LR
P[push main / dispatch] --> W[ci.yml full suite]
W --> C[checks]
C --> D[deploy job: runs-on host na workstation]
D --> S[ssh homeserver bash -s < scripts/deploy-homeserver.sh SHA]
S --> G[HEAD guard: main remoto == SHA?]
G --> B[build no homeserver: runner + migrator stages<br/>secrets do teqo-1313.env, --network stack_default]
B --> R[push localhost:5000 + tag teqo-1313:SHA]
R --> M[migrate: compose --profile maintenance run --rm teqo-1313-migrate]
M --> U[compose up -d teqo-1313 + healthcheck]
U --> K[smoke: / , /campanha/login, /admin, 307, webauthn, revalidate]
```
**Opções consideradas:**
- **A) Deploy job `runs-on: host` na workstation; TODO o deploy executa no homeserver via SSH (`bash -s` com o script do repo), build lá mesmo com secrets lidos do `~/stack/teqo-1313.env` local.** Recomendada — porque: o build precisa (1) do DB de produção para a geração estática — `postgres:5432` só existe dentro da rede do compose (`stack_default`; o postgres não publica porta); (2) dos secrets (`DATABASE_URL`, `PAYLOAD_SECRET`, `NEXT_PUBLIC_SITE_URL`) que já vivem chmod-600 no homeserver; (3) do registry em `localhost:5000` (loopback HTTP funciona — verificado: 401 de auth, não erro de transporte). Com tudo no homeserver, **zero secrets novos no Forgejo** e zero mudança de infra no compose. `runs-on: host` usa a chave SSH existente da workstation (verificada) e mantém o job sem segredos.
- **B) Build na workstation com DB via túnel SSH + secrets no Forgejo.** Exige publicar o postgres do homeserver (mudança de infra no compose), colocar `DATABASE_URL`/`PAYLOAD_SECRET` de produção como secrets do Forgejo (pior superfície de vazamento), túnel SSH no job e BuildKit com `--network host` para alcançar o túnel. **Rejeitada** — pior em todos os eixos exceto CPU (e a build no laptop cabe: 8c, ~12GB livres medidos; o stack ocioso usa ~3GB; o manual de 21b3c00d já provou o Dockerfile lá).
- **C) Job no container (ubuntu-latest) com chave SSH nova em secret do Forgejo.** Mais isolamento formal, mas: gestão de chave/known_hosts, alcance tailnet do container do runner a verificar, e o mesmo SSH remote de qualquer forma. **Rejeitada** — o ator é o dono único da instância (só repos fsolla/amana), o runner já roda como `fsolla` no host, e a mitigação real é não expor o job a eventos não confiáveis (abaixo).
**Decisões de engenharia (com rejeitadas):**
1. **Job `deploy` apenas em `ci.yml`** (push em `main` + `workflow_dispatch`), `needs: [checks]`, `timeout-minutes: 60`, `runs-on: host`. Nunca em `ci-pr.yml`. Guard de segurança: verificar na implementação que o Forgejo não executa workflows de PRs de fork (default off; se estiver ligado, desligar é passo humano no runbook). Sem isso, um PR de fork malicioso poderia rodar código no host da workstation.
2. **Guard de HEAD remoto (análogo ao cooldown do Vercel):** o script aborta (exit 0 "stale") se `git ls-remote <repo> refs/heads/main` ≠ SHA do job — um push mais novo vence; o deploy antigo não sobrepõe. Serialização: `flock` em `/tmp/teqo-1313-deploy.lock` no homeserver (capacity 1 + sem concurrency no Forgejo já serializam; o flock cobre dispatch+push simultâneos e re-deploys manuais).
3. **Script único POSIX-ish `scripts/deploy-homeserver.sh`** versionado neste repo, executado remotamente (`ssh homeserver "bash -s -- <sha>" < scripts/deploy-homeserver.sh`). O homeserver não ganha dependência nenhuma (só bash + git + docker, tudo presente). Lógica: HEAD guard → flock → source dos envs (`~/stack/teqo-1313.env` + `~/stack/.env`, sem eco) → workspace `~/teqo-deploy` (clone/fetch do Forgejo local `http://localhost:3000/fsolla/teqo.git`, público) → `docker login localhost:5000` (REGISTRY_USER/PASSWORD do `.env`) → build dos dois stages (`--network stack_default`, `--secret id=database_url,env=DATABASE_URL` + `payload_secret`, `--build-arg NEXT_PUBLIC_SITE_URL`) → push → backup do compose (`docker-compose.yml.pre-$SHA`) + swap dos tags de imagem via `sed` → **migrate** (`docker compose --profile maintenance run --rm teqo-1313-migrate` — o mesmo caminho do OPS51) → `docker compose up -d teqo-1313` → aguarda healthcheck healthy → smoke.
4. **Migrate:** serviço de maintenance do compose (`teqo-1313-migrate`, imagem `teqo-1313-migrator:<sha>` após o swap) — é o caminho que o compose já prevê e que o OPS51 usou. O `payload migrate` embutido no `pnpm build` não roda no Dockerfile (ele chama `next build` direto — verificado), então a ordem migrate→rollout é garantida pelo script.
5. **Smoke pós-deploy** (no homeserver, contra `localhost:1313`): `GET /` 200; `GET /campanha/login` 200; `GET /admin` 200; `GET /campanha` → 307 (barreira sem sessão); `POST /campanha/webauthn/login-options` 200 (anônimo por design); `POST /api/revalidate` com `x-revalidate-secret` do env → `{revalidated:true}`. Login com credenciais reais fica como validação manual (precedente OPS51).
6. **Falha → rollback automático best-effort:** restore do backup do compose + `docker compose up -d teqo-1313` (imagem anterior continua local e no registry; registry nunca deleta) + exit 1 (job vermelho). Caveat documentado: migration de schema aplicada no passo de migrate **não** é desfeita pelo rollback (Payload é append-only) — o código velho sobre schema novo é risco residual aceito; o caminho primário protege (migrate só roda após build+push ok; rollout só após migrate ok).
7. **Testes:** spec unit nova (`tests/unit/deployScript.unit.spec.ts`) com `bash -n` no script + greps estruturais que pinam as propriedades de segurança (HEAD guard, `--network stack_default`, `--secret id=database_url`, serviço de maintenance, endpoints do smoke) — mesmo padrão do `ciSkipInvariants` que pina os workflows. Os invariantes existentes de `ci.yml` (`.next-e2e` etc.) permanecem.
### Componentes / mudanças
- **`.forgejo/workflows/ci.yml`**: job `deploy` novo (runs-on `host`, `needs: [checks]`, checkout@v5 + `ssh homeserver "bash -s -- $GITHUB_SHA" < scripts/deploy-homeserver.sh`); comentário de cabeçalho atualizado ("No deploy step" → o passo existe agora).
- **`scripts/deploy-homeserver.sh`** (novo): o script remoto descrito acima; args = SHA do commit; env de configuração com default (`TEQO_REPO_URL`, `STACK_DIR`, `WORKSPACE`, `LOCK`); nunca ecoa valores de env; `set -euo pipefail`.
- **`tests/unit/deployScript.unit.spec.ts`** (novo): `bash -n` + invariantes estruturais.
- **`docs/ops/teqo-1313-deploy.md`** (novo, dir novo): runbook — gatilho, fluxo, onde roda cada passo, rollback (restore do backup do compose + `up -d`, imagens anteriores no registry), re-deploy manual (dispatch), falhas conhecidas, quem é o humano (secrets ficam no homeserver; nada novo no Forgejo).
- **`AGENTS.md`** (parágrafo de produção/CI) e **`docs/AGENT-OPS.md`** (linha do fluxo + tabela de ambientes): deploy deixou de ser "runbook fora do repo".
- **`docs/changelog/2026-08-17-ops53.md`** + `pnpm changelog:build` (OPS44).
- **infra-solla** (fora do repo, anotação no fechamento): `STATE.md` e runbook com a correção da topologia (runner na workstation, não no homeserver) e o passo único de setup humano — nada de daemon.json (loopback HTTP OK), nada de secrets novos.
- **Migration:** sem migration (não toca schema).
- **Access / Consent:** N/A.
- **UI:** Impeccable A — sem UI.
### Dados → forma
N/A (sem dados novos na UI).
## Fases verificáveis
1. **Tracer — script + workflow + tests + docs** — escrever `deploy-homeserver.sh`, job no `ci.yml`, spec unit, runbook, atualizações de AGENTS/AGENT-OPS, changelog. Gates: `pnpm gate:fast`, `pnpm format:check`, `pnpm knip`, `pnpm check:cycles`.
2. **Rehearsal read-only** — rodar os comandos do smoke contra o site **atual** (a partir do homeserver) para validar a lógica do smoke sem deploy; `bash -n`; verificar o setting de fork-PRs no Forgejo (mitigação do `runs-on: host`).
3. **First real deploy = o merge do próprio OPS53** — o pipeline publica o SHA do merge; acompanhar o job; se falhar, rollback pelo runbook (restore do backup + `up -d`). Este é o tracer bullet do aceite ("merge == publicar").
## Rabbit holes / Não escopo (engenharia)
- Blue-green / zero-downtime obrigatório — aceite é a janela do recreate; evolução futura.
- Deploy button / UI de rollback — runbook basta.
- Build na workstation com túnel para ganhar CPU — vira item futuro **se** a build no laptop incomodar (gatilho: OOM ou >30 min).
- Registry com `latest`/tags rolantes — tags por SHA + retenção bastam.
- Backup de DB no deploy — o plano infra já aponta pg_dump diário como pendência (débito registrado no fechamento, não neste item).
- Otimizar o job para "só deploy se o diff tocar código" — idempotente, custo aceito.
## Riscos e mitigação
- **OOM na build do laptop (8c/16GB, ~12GB livres).** O manual de 21b3c00d já buildou lá; teqo-1313 tem mem_limit 3g; mitigação: monitorar o primeiro deploy; gatilho de revisitação = build na workstation via túnel (registrado acima).
- **`runs-on: host` = código executando como fsolla na workstation.** Mitigação: job só em ci.yml (push main/dispatch — main é protegida, só entra via PR com checks verdes); verificar fork-PRs desligado no Forgejo (passo 2); instance é do dono único (repos fsolla/amana).
- **`actions/checkout@v5` em job `host`** — se o checkout falhar no host label, fallback documentado: baixar o script via raw do Forgejo (`git.solla.dev/fsolla/teqo/raw/branch/main/scripts/deploy-homeserver.sh`).
- **Rollback pós-migrate** — código velho + schema novo pode não ser 100% funcional; best-effort + vermelho no job + runbook; caminho primário minimiza a janela.
- **Vazamento de secrets nos logs** — script sem `set -x`, valores só via env do `~/stack/*.env`, build com BuildKit `--secret` (sem camadas); smoke do revalidate sem ecoar o secret.
- **Deploy antigo sobrescrevendo push novo** — HEAD guard remoto (stale → skip verde).
## Aceite de engenharia
- [x] Aceite de produto da intenção ainda coberto (merge == publicar; migrate → rollout; rollback documentado; falha = vermelho; sem build no laptop do dev)
- [x] Invariantes AGENTS/engineering-standards (sem segredos no repo; guards de dev/test intocados; nada de Neon)
- [x] Testes previstos: spec unit de `bash -n` + invariantes estruturais do script; invariantes existentes dos workflows intactos
- [x] Docs: runbook de deploy/rollback + AGENTS/AGENT-OPS + changelog
Self-score decision-quality: 1) decisões caras com rejeitadas ✓ 2) cabe no appetite ✓ 3) rabbit holes nomeados ✓ 4) reusa padrões existentes (Dockerfile/migrator, serviço de maintenance, smoke do OPS51) ✓ 5) aceite da intenção preservado ✓ — 5/5.

View file

@ -0,0 +1,169 @@
#!/usr/bin/env bash
#
# Deploy teqo-1313 to the homeserver (OPS53).
#
# Runs ON the homeserver, piped by the CI deploy job:
# ssh homeserver "bash -s -- <commit-sha>" < scripts/deploy-homeserver.sh
#
# Flow: HEAD guard (only the current main HEAD deploys) -> flock
# serialization -> workspace fetch at <sha> -> docker login (local registry)
# -> build of the runner + migrator stages (BuildKit secrets, compose
# network) -> push to localhost:5000 -> compose image-tag swap (with backup)
# -> migrate via the maintenance service (BEFORE the rollout) -> 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.
#
# Environment defaults assume the homeserver layout: stack under
# $HOME/stack, repo cloned from the local Forgejo. Secrets are sourced from
# the chmod-600 env files and never echoed (no `set -x`, passwords only via
# --password-stdin / build secrets).
set -euo pipefail
SHA="${1:?usage: deploy-homeserver.sh <commit-sha>}"
TEQO_REPO_URL="${TEQO_REPO_URL:-http://localhost:3000/fsolla/teqo.git}"
STACK_DIR="${STACK_DIR:-$HOME/stack}"
WORKSPACE_DIR="${WORKSPACE_DIR:-$HOME/teqo-deploy}"
DEPLOY_LOCK="${DEPLOY_LOCK:-/tmp/teqo-1313-deploy.lock}"
say() { printf '[deploy] %s\n' "$*"; }
fatal() {
trap - ERR
say "FAILED: $*" >&2
exit 1
}
# --- guards ------------------------------------------------------------
main_head="$(git ls-remote "$TEQO_REPO_URL" refs/heads/main | awk '{print $1}')"
if [ "$main_head" != "$SHA" ]; then
say "stale run: main is $main_head, job deploys $SHA — skipping"
exit 0
fi
exec 9>"$DEPLOY_LOCK"
flock -w 3600 9 || fatal "another deploy holds $DEPLOY_LOCK"
main_head="$(git ls-remote "$TEQO_REPO_URL" refs/heads/main | awk '{print $1}')"
if [ "$main_head" != "$SHA" ]; then
say "stale run after lock: main is $main_head, job deploys $SHA — skipping"
exit 0
fi
# --- secrets (sourced locally, never echoed) ----------------------------
set -a
. "$STACK_DIR/teqo-1313.env"
. "$STACK_DIR/.env"
set +a
: "${DATABASE_URL:?teqo-1313.env is missing DATABASE_URL}"
: "${PAYLOAD_SECRET:?teqo-1313.env is missing PAYLOAD_SECRET}"
: "${NEXT_PUBLIC_SITE_URL:?teqo-1313.env is missing NEXT_PUBLIC_SITE_URL}"
: "${REVALIDATE_SECRET:?teqo-1313.env is missing REVALIDATE_SECRET}"
: "${REGISTRY_USER:?stack/.env is missing REGISTRY_USER}"
: "${REGISTRY_PASSWORD:?stack/.env is missing REGISTRY_PASSWORD}"
# --- workspace at <sha> -------------------------------------------------
if [ ! -d "$WORKSPACE_DIR/.git" ]; then
say "cloning $TEQO_REPO_URL into $WORKSPACE_DIR"
git clone "$TEQO_REPO_URL" "$WORKSPACE_DIR"
else
git -C "$WORKSPACE_DIR" fetch origin main
fi
git -C "$WORKSPACE_DIR" checkout --detach "$SHA" || fatal "checkout of $SHA failed"
say "workspace at $SHA"
# --- registry -----------------------------------------------------------
say "logging into local registry localhost:5000"
echo "$REGISTRY_PASSWORD" | docker login localhost:5000 --username "$REGISTRY_USER" --password-stdin >/dev/null
# --- build --------------------------------------------------------------
build_image() {
local target="$1" image="$2"
say "building $target ($image)"
DOCKER_BUILDKIT=1 docker build \
--network stack_default \
--build-arg "NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL" \
--secret "id=database_url,env=DATABASE_URL" \
--secret "id=payload_secret,env=PAYLOAD_SECRET" \
--target "$target" \
-t "$image" .
}
cd "$WORKSPACE_DIR"
build_image runner "localhost:5000/teqo-1313:$SHA"
build_image migrator "localhost:5000/teqo-1313-migrator:$SHA"
say "pushing images to localhost:5000"
docker push "localhost:5000/teqo-1313:$SHA"
docker push "localhost:5000/teqo-1313-migrator:$SHA"
# The compose references bare `teqo-1313:<sha>` tags with pull_policy: never.
docker tag "localhost:5000/teqo-1313:$SHA" "teqo-1313:$SHA"
docker tag "localhost:5000/teqo-1313-migrator:$SHA" "teqo-1313-migrator:$SHA"
# --- compose swap (backup first; failures from here roll back) ----------
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|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)"
rollback() {
trap - ERR
say "FAILED: $* — restoring previous compose and image"
cp "$backup" "$compose" 2>/dev/null || true
( cd "$STACK_DIR" && docker compose up -d teqo-1313 ) 2>/dev/null || true
exit 1
}
trap 'rollback "unexpected failure"' ERR
# --- migrate (before the rollout) ---------------------------------------
cd "$STACK_DIR"
say "applying pending migrations (maintenance service teqo-1313-migrate)"
docker compose --profile maintenance run --rm teqo-1313-migrate || rollback "migrations failed"
# --- rollout ------------------------------------------------------------
say "rolling out teqo-1313"
docker compose up -d teqo-1313 || rollback "compose up failed"
say "waiting for the healthcheck to go healthy"
health="starting"
for _ in $(seq 1 30); do
health="$(docker inspect -f '{{.State.Health.Status}}' teqo-1313 2>/dev/null || true)"
[ "$health" = "healthy" ] && break
[ "$health" = "unhealthy" ] && break
sleep 10
done
[ "$health" = "healthy" ] || rollback "container not healthy after 300s (status: $health)"
# --- smoke --------------------------------------------------------------
base="http://localhost:1313"
smoke_fail() { rollback "smoke: $*"; }
curl -fsS -o /dev/null "$base/" || smoke_fail "GET /"
curl -fsS -o /dev/null "$base/campanha/login" || smoke_fail "GET /campanha/login"
curl -fsS -o /dev/null "$base/admin" || smoke_fail "GET /admin"
code="$(curl -s -o /dev/null -w '%{http_code}' "$base/campanha" || true)"
[ "$code" = "307" ] || smoke_fail "GET /campanha expected 307, got $code"
curl -fsS -o /dev/null -X POST "$base/campanha/webauthn/login-options" \
-H 'Content-Type: application/json' -d '{}' || smoke_fail "POST webauthn login-options"
body="$(curl -fsS -X POST "$base/api/revalidate" -H "x-revalidate-secret: $REVALIDATE_SECRET" || true)"
echo "$body" | grep -q '"revalidated":true' || smoke_fail "POST /api/revalidate did not confirm"
say "deploy of $SHA complete: $(docker inspect -f '{{.Image}}' teqo-1313)"

View file

@ -0,0 +1,65 @@
// @vitest-environment node
import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
const repoRoot = join(fileURLToPath(new URL('.', import.meta.url)), '..', '..')
const scriptPath = join(repoRoot, 'scripts', 'deploy-homeserver.sh')
const script = readFileSync(scriptPath, 'utf8')
describe('scripts/deploy-homeserver.sh (OPS53 deploy pipeline)', () => {
it('is valid bash', () => {
expect(() => execFileSync('bash', ['-n', scriptPath], { stdio: 'pipe' })).not.toThrow()
})
it('guards against stale runs: only the current main HEAD deploys', () => {
expect(script).toContain('git ls-remote')
expect(script).toContain('refs/heads/main')
expect(script).toContain('stale run')
})
it('builds with BuildKit secrets — prod credentials never enter image layers', () => {
expect(script).toContain('--secret')
expect(script).toContain('id=database_url,env=DATABASE_URL')
expect(script).toContain('id=payload_secret,env=PAYLOAD_SECRET')
})
it('builds inside the compose network so the static generation reaches the prod DB', () => {
expect(script).toContain('--network stack_default')
})
it('applies migrations through the maintenance service before the rollout', () => {
// The rollback helper also contains a `compose up -d` (before the migrate
// step); the REAL rollout is the last occurrence.
const migrateIndex = script.indexOf('teqo-1313-migrate')
const upIndex = script.lastIndexOf('docker compose up -d teqo-1313')
expect(migrateIndex).toBeGreaterThan(-1)
expect(upIndex).toBeGreaterThan(-1)
expect(migrateIndex).toBeLessThan(upIndex)
})
it('smokes the deployed surface with the prod revalidate secret', () => {
for (const fragment of [
'/campanha/login',
'/admin',
'/campanha/webauthn/login-options',
'/api/revalidate',
'x-revalidate-secret',
]) {
expect(script, fragment).toContain(fragment)
}
})
it('never echoes secret values (no set -x, passwords only via stdin/secrets)', () => {
expect(script).not.toMatch(/^set -x\b/m)
expect(script).not.toContain('echo "$DATABASE_URL"')
expect(script).not.toContain('echo "$PAYLOAD_SECRET"')
// Registry password goes to docker via stdin — never stdout, never argv.
expect(script).toContain('echo "$REGISTRY_PASSWORD" | docker login')
expect(script).not.toContain('--password "$REGISTRY_PASSWORD"')
})
})