From ca9a5cb60e2f433523a8937a68bef8d86a00e516 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Mon, 14 Sep 2026 08:01:59 -0300 Subject: [PATCH] =?UTF-8?q?fix(OPS107):=20aprova=C3=A7=C3=A3o=20de=20produ?= =?UTF-8?q?=C3=A7=C3=A3o=20n=C3=A3o=20ocupa=20a=20lane=20do=20staging=20(C?= =?UTF-8?q?loses=20#1002)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 46 +++--- AGENTS-infra.md | 2 +- AGENTS.md | 2 +- README.md | 6 +- docs/AGENT-OPS.md | 22 +-- docs/changelog/2026-09-14-ops107.md | 1 + docs/ops/teqo-1313-deploy.md | 35 +++-- ...ps107-aprovacao-producao-escolhida-impl.md | 132 ++++++++++++++++++ scripts/deploy-homeserver.sh | 6 +- tests/unit/ciSkipInvariants.unit.spec.ts | 26 +++- tests/unit/deployScript.unit.spec.ts | 5 +- 11 files changed, 229 insertions(+), 54 deletions(-) create mode 100644 docs/changelog/2026-09-14-ops107.md create mode 100644 docs/plans/ops107-aprovacao-producao-escolhida-impl.md diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d31ce426..f2efebc6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,5 +1,5 @@ # Deploy (OPS71 — GitHub Actions; staging + production since OPS103; -# automatic start since OPS104). +# automatic start since OPS104; approval lane decoupled since OPS107). # # STARTS BY ITSELF: a `push` to `main` starts the run (OPS104 trigger 1) — # the `preflight` job skips it when another deploy run is already @@ -28,6 +28,10 @@ # (required reviewer fsolla, configured in the repo). # A red staging fail-closes production. Same script, # TEQO_ENV=production (teqo-1313, 127.0.0.1:1313). +# NO concurrency group (OPS107): a job `waiting` on the +# approval must not occupy the shared deploy lane — the +# concurrency is evaluated before the environment gate, +# and holding the group blocked every later run's staging. # requeue — hosted, after deploy-staging: re-dispatches when main # advanced (OPS104 trigger 2). # @@ -42,11 +46,12 @@ # pre-build on the env DB — OPS66 — and the compose network / local registry # localhost:5000), which is exactly why the hosted jobs only verify/decide # and the self-hosted jobs only build + deploy. Deploy is idempotent -# ("already deployed" guard in deploy-homeserver.sh) and serialized by flock -# per environment; the `concurrency` group below serializes the two shared -# homeserver compose/workspace consumers across runs without cancelling an -# in-flight deploy (the staged `needs` chain already serializes them inside -# one run). +# ("already deployed" guard in deploy-homeserver.sh) and serialized by ONE +# flock on the host (both environments). The `concurrency` group below lives +# ONLY on `deploy-staging` (OPS107) — it never waits on an approval, so it +# always drains; the staged `needs` chain serializes the environments inside +# one run. Production execution is serialized by that same chain, the single +# self-hosted runner and the host flock — never by a GitHub group. # # The deploy jobs fetch the repo from github.com (public) — TEQO_REPO_URL in # deploy-homeserver.sh defaults there; the script runs to completion for the @@ -211,11 +216,14 @@ jobs: runs-on: [self-hosted, homeserver] timeout-minutes: 60 concurrency: - # Shared homeserver compose/workspace: never two deploys at once across - # runs. The staged needs chain serializes them within one run. `queue: - # max`: the default single-slot queue cancels a previously pending - # deploy when a new dispatch arrives, even with cancel-in-progress - # false — deploys must wait their turn (OPS102: run to completion). + # Shared homeserver compose/workspace: never two staging deploys at once + # across runs. Only staging holds this group (OPS107): it never waits on + # an approval, so it always drains — production has NO group because a + # `waiting` approval would block every later run's staging (the + # concurrency is evaluated before the environment gate). `queue: max`: + # the default single-slot queue cancels a previously pending deploy when + # a new dispatch arrives, even with cancel-in-progress false — deploys + # must wait their turn (OPS102: run to completion). group: deploy-homeserver cancel-in-progress: false queue: max @@ -250,24 +258,24 @@ jobs: deploy-production: name: deploy production (teqo-1313) - # OPS53 + OPS71 + OPS103 + OPS104. Runs on the GitHub self-hosted runner - # ON the homeserver (labels self-hosted + homeserver): the script executes - # locally — no SSH hop, no workstation involvement. Never wired in + # OPS53 + OPS71 + OPS103 + OPS104 + OPS107. Runs on the GitHub self-hosted + # runner ON the homeserver (labels self-hosted + homeserver): the script + # executes locally — no SSH hop, no workstation involvement. Never wired in # ci-pr.yml: a self-hosted job is code execution on the production # machine, and only a run with the full verify suite green can reach it # (the automatic start and the manual dispatch share the same gate). # `needs: [deploy-staging]` makes a red staging fail-close production; the # environment `production` (required reviewer, configured in the repo) is - # the approval gate — and the ONLY one. + # the approval gate — and the ONLY one. No concurrency group (OPS107): a + # job `waiting` here must not hold the shared deploy lane (it blocked every + # later run's staging, forcing old runs to be approved to reach the + # newest). Execution stays serialized by the needs chain, the single + # self-hosted runner and the host flock in deploy-homeserver.sh. needs: [deploy-staging] if: needs.deploy-staging.result == 'success' && github.ref == 'refs/heads/main' environment: production runs-on: [self-hosted, homeserver] timeout-minutes: 60 - concurrency: - group: deploy-homeserver - cancel-in-progress: false - queue: max env: TEQO_ENV: production steps: diff --git a/AGENTS-infra.md b/AGENTS-infra.md index 90556378..6fee88c9 100644 --- a/AGENTS-infra.md +++ b/AGENTS-infra.md @@ -2,7 +2,7 @@ ## Deploy & CI -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). **CI/PR/deploy AND the ISSUE TRACKER live on GitHub (OPS71 + OPS76, 2026-08-21)** (`github.com/fsolla/teqo/issues` — labels, claims, `pnpm issue`/`agent:*` and the post-merge flips talk to the GitHub API; GitHub is the single host). The PR CI (`ci-pr.yml`, job único `checks` — OPS62) runs the same cascade as always (guards → lint → format → typecheck → knip → cycles → unit → int → build → e2e selected/afetado, skips via `ci-scope.mjs`); its check-run `CI (PR) / checks` is the required check of `main`. **There is NO main verifier (`ci.yml`) anymore**: a merge to `main` starts the deploy run automatically (OPS104 supersedes the OPS102 manual-only anti-goal; `workflow_dispatch` stays as the manual escape) — the verifier IS the deploy's `verify` job. The deploy workflow has five jobs: `preflight` (hosted; `scripts/deploy-preflight.mjs` skips the run when another deploy is `queued`/`in_progress` — a `waiting` production approval does not count) → `verify` (hosted `ubuntu-latest`, suíte **full** incl. e2e full, rodado uma vez por commit) → `deploy-staging` (`environment: staging`, OPS103) → `requeue` (hosted, outside the deploy concurrency group; `scripts/deploy-requeue.mjs` re-dispatches a fresh run when `main` advanced — OPS104) and `deploy-production` (`environment: production` com required reviewer, `needs: [deploy-staging]`): the GitHub self-hosted runner ON the homeserver executes `scripts/deploy-homeserver.sh` **locally** (no SSH; outbound-only runner, works behind the tunnel; the hosted job never touches the homeserver). O script é único e parametrizado por `TEQO_ENV` (`production` default intocado; `staging` = `teqo-staging`/`teqo_staging`/`~/stack/teqo-staging.env`/`127.0.0.1:1314`, bucket `teqo-media-staging`; envs de staging só no homeserver, nunca como GitHub secrets; staging não é indexável). The remote Docker deployment is gone; the production container runs the monolith directly. The Docker Compose file is used ONLY for local dev databases. The `run-deploy.yml` (OPS38/OPS46) was deleted in OPS71. There is no `run-deploy.yml` or equivalent in GitHub Actions — deploy is `deploy.yml` only. +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). **CI/PR/deploy AND the ISSUE TRACKER live on GitHub (OPS71 + OPS76, 2026-08-21)** (`github.com/fsolla/teqo/issues` — labels, claims, `pnpm issue`/`agent:*` and the post-merge flips talk to the GitHub API; GitHub is the single host). The PR CI (`ci-pr.yml`, job único `checks` — OPS62) runs the same cascade as always (guards → lint → format → typecheck → knip → cycles → unit → int → build → e2e selected/afetado, skips via `ci-scope.mjs`); its check-run `CI (PR) / checks` is the required check of `main`. **There is NO main verifier (`ci.yml`) anymore**: a merge to `main` starts the deploy run automatically (OPS104 supersedes the OPS102 manual-only anti-goal; `workflow_dispatch` stays as the manual escape) — the verifier IS the deploy's `verify` job. The deploy workflow has five jobs: `preflight` (hosted; `scripts/deploy-preflight.mjs` skips the run when another deploy is `queued`/`in_progress` — a `waiting` production approval does not count) → `verify` (hosted `ubuntu-latest`, suíte **full** incl. e2e full, rodado uma vez por commit) → `deploy-staging` (`environment: staging`, OPS103) → `requeue` (hosted, outside the deploy concurrency group; `scripts/deploy-requeue.mjs` re-dispatches a fresh run when `main` advanced — OPS104) and `deploy-production` (`environment: production` com required reviewer, `needs: [deploy-staging]`; OPS107: sem `concurrency` — o approval pendente não segura o staging nem a aprovação de runs novos, então aprove só o run escolhido e rejeite os demais): the GitHub self-hosted runner ON the homeserver executes `scripts/deploy-homeserver.sh` **locally** (no SSH; outbound-only runner, works behind the tunnel; the hosted job never touches the homeserver). O script é único e parametrizado por `TEQO_ENV` (`production` default intocado; `staging` = `teqo-staging`/`teqo_staging`/`~/stack/teqo-staging.env`/`127.0.0.1:1314`, bucket `teqo-media-staging`; envs de staging só no homeserver, nunca como GitHub secrets; staging não é indexável). The remote Docker deployment is gone; the production container runs the monolith directly. The Docker Compose file is used ONLY for local dev databases. The `run-deploy.yml` (OPS38/OPS46) was deleted in OPS71. There is no `run-deploy.yml` or equivalent in GitHub Actions — deploy is `deploy.yml` only. **Auto-unblock do `verify` (OPS106):** um `verify` vermelho de push em `main` dispara UM agente autônomo de desbloqueio no homeserver — workflow separado `.github/workflows/auto-unblock.yml` (`workflow_run` no Deploy, push-only, conferindo pela jobs API que o job `verify` falhou), `scripts/auto-unblock.sh` (flock `/tmp/teqo-unblock.lock`) + `scripts/auto-unblock.mjs` (token Issue `auto-unblock` = single-flight; falha com agente ativo só comenta) e `scripts/auto-unblock-agent.mjs` destacado (`detached`, fd 9 herdado, sem `RUNNER_TRACKING_ID`; cap 4h) que provisiona `fix/*` via `scripts/worktree.mjs fix --headless`, aplica o guard de banco local `teqo_wt*`/`*_test` e roda `opencode run --command bug-fix`. O agente faz push/PR com o `AUTOMERGE_PAT` por credential helper não-persistente; `deploy.yml`/`verify` continuam intocados (sem auto-retry, produção segue com approval humano). Bootstrap + operação: `docs/ops/teqo-1313-deploy.md` (seção Auto-unblock); smoke `pnpm unblock:check`. diff --git a/AGENTS.md b/AGENTS.md index 1b162db4..6e8cf442 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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). **CI/PR/deploy AND the ISSUE TRACKER live on GitHub (OPS71 + OPS76, 2026-08-21)** (`github.com/fsolla/teqo/issues` — labels, claims, `pnpm issue`/`agent:*` and the post-merge flips talk to the GitHub API; GitHub is the single host). The PR CI (`ci-pr.yml`, job único `checks` — OPS62) runs the same cascade as always (guards → lint → format → typecheck → knip → cycles → unit → int → build → e2e selected/curated, skips via `ci-scope.mjs`; OPS86: diff high-risk roda o conjunto curado de e2e — nunca zero —, área de risco sem mapping falha fechado `unmapped-risk`, e unit/int `changed` com seleção vazia cai em full via `vitest-changed-or-full.mjs`); its check-run `CI (PR) / checks` is the required check of `main` — literal de match na proteção: `checks` (o GitHub casa pelo nome do check-run/job; a UI exibe `CI (PR) / checks` — pin ao vivo PR #742) — and the gate of the **native auto-merge** (`agent-pr-ready-automerge.yml` → `scripts/github-pr-automerge.mjs`, GraphQL `enablePullRequestAutoMerge`, rebase — armed with the `AUTOMERGE_PAT` real-user PAT, fail-closed without it; the built-in GITHUB*TOKEN would make the merge happen as `github-actions[bot]`, whose events never create workflow runs — the post-merge `closed` flips would silently never run, live finding PR #746 (OPS71-FLIP) — the server only merges with the required check green). **There is NO main verifier (`ci.yml`) anymore**: a merge to `main` starts the deploy run automatically (OPS104 supersedes the OPS102 manual-only anti-goal; `workflow_dispatch` stays as the manual escape) — the verifier IS the deploy's `verify` job. The workflow (`.github/workflows/deploy.yml`) now has five jobs: `preflight` (hosted; skips the run when another deploy is `queued`/`in_progress` — a `waiting` production approval does not count) → `verify` (hosted `ubuntu-latest`, suíte **full** incl. e2e full, intocado e rodado uma vez por commit) → `deploy-staging` (`environment: staging`, sem reviewer) → `requeue` (hosted, outside the deploy concurrency group; re-dispatches a fresh run when `main` advanced during the run — OPS104) and `deploy-production` (`environment: production` com required reviewer, `needs: [deploy-staging]`; staging vermelho não deixa produção rodar — OPS103): the GitHub self-hosted runner ON the homeserver executes `scripts/deploy-homeserver.sh` **locally** (no SSH; outbound-only runner, works behind the tunnel; the hosted job never touches the homeserver). O script é único e parametrizado por `TEQO_ENV` (`production` default intocado; `staging` = `teqo-staging`/`teqo_staging`/`~/stack/teqo-staging.env`/`127.0.0.1:1314`, bucket `teqo-media-staging`; envs de staging vivem só no homeserver, nunca como GitHub secrets; staging não é indexável). The remote script is idempotent ("already deployed" → skip), builds the migrator image first, applies pending migrations through the compose maintenance service `teqo-1313-migrate` **before the runner build** (OPS66: the static build reads the new schema — build-after-migrate, never the reverse), builds the standalone image at the pushed SHA (BuildKit secrets from the environment's env file), pushes it to the local registry (`localhost:5000`), and recreates the container; the repo comes from `TEQO_REPO_URL` (default `https://github.com/fsolla/teqo.git`, public). `pnpm push` pushes to `origin` — **the GitHub remote** (one-time local cutover: `git remote set-url origin git@github.com:fsolla/teqo.git`; the Forgejo stays as a frozen secondary repo at the cutover point, its git repo and issue tracker inactive — OPS76 migrated the tracker to GitHub). The Forgejo-era files (`.forgejo/workflows/`, `forgejo-pr-automerge.mjs`, `forgejo-dispatch.mjs`, `ci-classify-production.mjs`) were removed in the OPS71 Fase 2 (2026-08-19); rollback to the Forgejo CI flow is a revert of that removal — the frozen Forgejo `main` still holds the full pre-cutover tree. Runbook (rollback, falhas conhecidas, passos do cutover): `docs/ops/teqo-1313-deploy.md`. **The agent-pool supervisor workflow was killed in OPS65** (the `*/10`tick cost 144 full`pnpm install`/day on the workstation; `agent-pool.yml` is gone, pool scripts/skills stay dormant). **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 moved to the homeserver Garage S3 in OPS52 (2026-08-18): the `media`collection uploads via`@payloadcms/storage-s3`when the`S3\*\*`envs are set, and Payload proxies the files through`/api/media/file/...`(the bucket stays private). Production env lives in`~/stack/teqo-1313.env`(bucket`teqo-media`, endpoint `http://host.docker.internal:3900` — o container alcança o Garage no host via `extra_hosts: host-gateway` + regras ufw `allow from 10.0.11.0/24 to any port 3900` (INPUT e route/FWD); a workstation/tailnet usa `http://100.119.220.31:3900`, key `teqo-key`); a partial `S3\__`config aborts the boot, and no`S3\_\*`means local disk (dev/test). The 40 pre-existing media rows are RECOVERED (OPS52-media, 2026-08-18): 38/40 objects restaurados no bucket a partir do WordPress (38 capas servindo 200 em`/api/media/file/...`; exceções sem fonte: `fim-escala-6x1.jpg`e`jorgesolla.jpg`) — re-executar via `pnpm media:recover`(restaura objetos sem tocar no DB; runbook em`docs/plans/ops52-media-recuperar-arquivos-impl.md`; desde OPS52-media-guard o reconcile exige `MEDIA_RECOVER_CONFIRM=1`— guard de intenção explícita para escrita no bucket,`--dry-run`/`--verify` seguem sem flag); the URLs themselves are the same contract the proxy now serves. 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). **CI/PR/deploy AND the ISSUE TRACKER live on GitHub (OPS71 + OPS76, 2026-08-21)** (`github.com/fsolla/teqo/issues` — labels, claims, `pnpm issue`/`agent:*` and the post-merge flips talk to the GitHub API; GitHub is the single host). The PR CI (`ci-pr.yml`, job único `checks` — OPS62) runs the same cascade as always (guards → lint → format → typecheck → knip → cycles → unit → int → build → e2e selected/curated, skips via `ci-scope.mjs`; OPS86: diff high-risk roda o conjunto curado de e2e — nunca zero —, área de risco sem mapping falha fechado `unmapped-risk`, e unit/int `changed` com seleção vazia cai em full via `vitest-changed-or-full.mjs`); its check-run `CI (PR) / checks` is the required check of `main` — literal de match na proteção: `checks` (o GitHub casa pelo nome do check-run/job; a UI exibe `CI (PR) / checks` — pin ao vivo PR #742) — and the gate of the **native auto-merge** (`agent-pr-ready-automerge.yml` → `scripts/github-pr-automerge.mjs`, GraphQL `enablePullRequestAutoMerge`, rebase — armed with the `AUTOMERGE_PAT` real-user PAT, fail-closed without it; the built-in GITHUB*TOKEN would make the merge happen as `github-actions[bot]`, whose events never create workflow runs — the post-merge `closed` flips would silently never run, live finding PR #746 (OPS71-FLIP) — the server only merges with the required check green). **There is NO main verifier (`ci.yml`) anymore**: a merge to `main` starts the deploy run automatically (OPS104 supersedes the OPS102 manual-only anti-goal; `workflow_dispatch` stays as the manual escape) — the verifier IS the deploy's `verify` job. The workflow (`.github/workflows/deploy.yml`) now has five jobs: `preflight` (hosted; skips the run when another deploy is `queued`/`in_progress` — a `waiting` production approval does not count) → `verify` (hosted `ubuntu-latest`, suíte **full** incl. e2e full, intocado e rodado uma vez por commit) → `deploy-staging` (`environment: staging`, sem reviewer) → `requeue` (hosted, outside the deploy concurrency group; re-dispatches a fresh run when `main` advanced during the run — OPS104) and `deploy-production` (`environment: production` com required reviewer, `needs: [deploy-staging]`; staging vermelho não deixa produção rodar — OPS103; **OPS107: sem `concurrency` — o approval pendente não ocupa a lane compartilhada, então runs novos fazem staging e ficam aprováveis; aprove só o run escolhido e rejeite os demais**): the GitHub self-hosted runner ON the homeserver executes `scripts/deploy-homeserver.sh` **locally** (no SSH; outbound-only runner, works behind the tunnel; the hosted job never touches the homeserver). O script é único e parametrizado por `TEQO_ENV` (`production` default intocado; `staging` = `teqo-staging`/`teqo_staging`/`~/stack/teqo-staging.env`/`127.0.0.1:1314`, bucket `teqo-media-staging`; envs de staging vivem só no homeserver, nunca como GitHub secrets; staging não é indexável). The remote script is idempotent ("already deployed" → skip), builds the migrator image first, applies pending migrations through the compose maintenance service `teqo-1313-migrate` **before the runner build** (OPS66: the static build reads the new schema — build-after-migrate, never the reverse), builds the standalone image at the pushed SHA (BuildKit secrets from the environment's env file), pushes it to the local registry (`localhost:5000`), and recreates the container; the repo comes from `TEQO_REPO_URL` (default `https://github.com/fsolla/teqo.git`, public). `pnpm push` pushes to `origin` — **the GitHub remote** (one-time local cutover: `git remote set-url origin git@github.com:fsolla/teqo.git`; the Forgejo stays as a frozen secondary repo at the cutover point, its git repo and issue tracker inactive — OPS76 migrated the tracker to GitHub). The Forgejo-era files (`.forgejo/workflows/`, `forgejo-pr-automerge.mjs`, `forgejo-dispatch.mjs`, `ci-classify-production.mjs`) were removed in the OPS71 Fase 2 (2026-08-19); rollback to the Forgejo CI flow is a revert of that removal — the frozen Forgejo `main` still holds the full pre-cutover tree. Runbook (rollback, falhas conhecidas, passos do cutover): `docs/ops/teqo-1313-deploy.md`. **The agent-pool supervisor workflow was killed in OPS65** (the `*/10`tick cost 144 full`pnpm install`/day on the workstation; `agent-pool.yml` is gone, pool scripts/skills stay dormant). **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 moved to the homeserver Garage S3 in OPS52 (2026-08-18): the `media`collection uploads via`@payloadcms/storage-s3`when the`S3\*\*`envs are set, and Payload proxies the files through`/api/media/file/...`(the bucket stays private). Production env lives in`~/stack/teqo-1313.env`(bucket`teqo-media`, endpoint `http://host.docker.internal:3900` — o container alcança o Garage no host via `extra_hosts: host-gateway` + regras ufw `allow from 10.0.11.0/24 to any port 3900` (INPUT e route/FWD); a workstation/tailnet usa `http://100.119.220.31:3900`, key `teqo-key`); a partial `S3\__`config aborts the boot, and no`S3\_\*`means local disk (dev/test). The 40 pre-existing media rows are RECOVERED (OPS52-media, 2026-08-18): 38/40 objects restaurados no bucket a partir do WordPress (38 capas servindo 200 em`/api/media/file/...`; exceções sem fonte: `fim-escala-6x1.jpg`e`jorgesolla.jpg`) — re-executar via `pnpm media:recover`(restaura objetos sem tocar no DB; runbook em`docs/plans/ops52-media-recuperar-arquivos-impl.md`; desde OPS52-media-guard o reconcile exige `MEDIA_RECOVER_CONFIRM=1`— guard de intenção explícita para escrita no bucket,`--dry-run`/`--verify` seguem sem flag); the URLs themselves are the same contract the proxy now serves. 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. diff --git a/README.md b/README.md index 402cb4de..91cba6b8 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,12 @@ > 1. O agente roda `pnpm agent:claim` (ou `pnpm worktree next --issue N`) e pega a próxima Issue `ready` por prioridade (`prio:P0..P3`). O **tracker de Issues, o código, os PRs e o CI vivem no GitHub** (`github.com/fsolla/teqo`). > 2. Ele implementa, roda o fast gate (`lint + typecheck + unit`) e `pnpm push -u origin HEAD` — abre a PR para `main` (Ready, `Closes #N`). > 3. CI verde (`CI (PR) / checks`) → o safety net `agent-pr-ready-automerge.yml` arma o **auto-merge nativo** (rebase) — o servidor só mergea com o required check verde. -> 4. Deploy **começa automático**: o merge em `main` dispara o `deploy.yml` (suíte full no `verify`) → staging publica sozinho → **produção só com o approve do reviewer** no environment `production` (`jorgesolla1313.com.br`, runner self-hosted) — migrações aplicadas antes do rollout; o staging velho se atualiza via requeue (OPS104). `workflow_dispatch` segue como escape manual. +> 4. Deploy **começa automático**: o merge em `main` dispara o `deploy.yml` (suíte full no `verify`) → staging publica sozinho → **produção só com o approve do reviewer** no environment `production` (`jorgesolla1313.com.br`, runner self-hosted) — migrações aplicadas antes do rollout; o staging velho se atualiza via requeue (OPS104); desde a OPS107 o approval pendente não segura o staging nem a aprovação de runs novos — aprove só o run escolhido e rejeite os demais. `workflow_dispatch` segue como escape manual. > 5. Secrets humanos (uma vez): `GITHUB_TOKEN` (PAT com escopo `repo` + `issues: write`) para os scripts de agente/PR (`pnpm issue`/`agent:*`, `github-pr.mjs`); envs de prod em `~/stack/teqo-1313.env` no homeserver. Branch protection de `main` já aplicada (reaplicar: `pnpm configure:branch-protection`). > > Comandos: `pnpm agent:claim | agent:register | agent:prioritize | agent:file-miss | worktree next` e `pnpm db:seed:minimal`. > Labels: estado `ready|in-progress|blocked|done|in-prod`, `prio:P0..P3`, `kind:*`, `needs:migration|consent`, `requirements-changed`. -> **Agente faz sozinho:** claim → implementar → PR → main (o merge já inicia o deploy de staging). **Só humano:** aprovar a produção no environment do run, envs do homeserver, branch protection, runbook de rollback. +> **Agente faz sozinho:** claim → implementar → PR → main (o merge já inicia o deploy de staging). **Só humano:** aprovar a produção no environment do run (desde a OPS107, só o run escolhido — rejeite os demais), envs do homeserver, branch protection, runbook de rollback. > Tudo em detalhe: [`docs/AGENT-OPS.md`](docs/AGENT-OPS.md) · CI: `.github/workflows/ci-pr.yml` · Deploy: `.github/workflows/deploy.yml`. Teqo starts as the official digital platform for **deputado Jorge Solla** and evolves into a **white-label civic engagement platform** for politicians in Brazil. @@ -97,7 +97,7 @@ To change the schema: 3. Review and commit the generated files in `src/migrations/` (both `.ts` and `.json`, plus `index.ts`). 4. Apply it locally: `pnpm migrate` (check status anytime with `pnpm migrate:status`). -**Deploying to production:** merges to `main` trigger `deploy.yml` automatically (OPS104) — the `verify` job runs the full suite, `deploy-staging` publishes on its own, and `deploy-production` waits for the `production` environment approval. The remote script (`scripts/deploy-homeserver.sh`) builds on the homeserver and applies pending migrations through the compose maintenance service `teqo-1313-migrate` **before** the rollout. Do not run migrations against production by hand. Runbook (rollback, known failures): `docs/ops/teqo-1313-deploy.md`. +**Deploying to production:** merges to `main` trigger `deploy.yml` automatically (OPS104) — the `verify` job runs the full suite, `deploy-staging` publishes on its own, and `deploy-production` waits for the `production` environment approval (OPS107: the pending approval no longer holds the shared deploy lane, so later runs still stage and stay approvable — approve only the chosen run and reject the rest). The remote script (`scripts/deploy-homeserver.sh`) builds on the homeserver and applies pending migrations through the compose maintenance service `teqo-1313-migrate` **before** the rollout. Do not run migrations against production by hand. Runbook (rollback, known failures): `docs/ops/teqo-1313-deploy.md`. ### Running tests diff --git a/docs/AGENT-OPS.md b/docs/AGENT-OPS.md index f1c461e3..91f8629f 100644 --- a/docs/AGENT-OPS.md +++ b/docs/AGENT-OPS.md @@ -16,13 +16,13 @@ Agentes **nunca** recebem `DATABASE_URL` de prod/staging e nunca setam `ALLOW_RE ```text claim → feature branch → PR --base main (GitHub) → CI PR green (cascade + skips) → auto-merge nativo em main (rebase) -main → push dispara deploy.yml sozinho (ou workflow_dispatch manual) → preflight (pula se já há deploy ativo) → verify full verde → deploy-staging → aprovação do environment production → deploy-production; requeue re-despacha se main andou no meio (OPS53 + OPS103 + OPS104) +main → push dispara deploy.yml sozinho (ou workflow_dispatch manual) → preflight (pula se já há deploy ativo) → verify full verde → deploy-staging → aprovação do environment production → deploy-production; requeue re-despacha se main andou no meio (OPS53 + OPS103 + OPS104 + OPS107 — o approval de produção não ocupa a lane compartilhada; runs novos seguem staging e aprováveis) ``` **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` (GitHub) → `project-status`. `docs/roadmap.md` = legado congelado; fonte canônica = Issues do GitHub (`github.com/fsolla/teqo/issues`). - **Agente faz sozinho:** claim (pool-supervisor no pool; humano via `pnpm agent:claim` fora da sessão — OPS33: `worktree next` claima) → implementa → **e2e local afetado** (OPS72, discricionário: roda os e2e que criou + os da mesma superfície — `pnpm test:e2e:affected`; ver skill) → **`pnpm push`** (origin = GitHub) → PR via `node scripts/github-pr.mjs` **Ready** (nunca draft) com `Closes #N` → o safety net `agent-pr-ready-automerge.yml` arma o **auto-merge nativo do GitHub** (`enablePullRequestAutoMerge`, rebase) — o servidor mergea quando o required check `CI (PR) / checks` fica verde. Regra always-on: `.agents/rules/agent-pr-workflow.mdc`. Em Cursor Cloud: `ManagePullRequest` com `draft: false` (o default draft da tool **não** vale neste repo) — o merge continua pelo safety net. -- **Só humano:** aprovar o environment `production` no run (o único gate de publicação — OPS104 nunca auto-aprova), envs de produção no homeserver (`~/stack/teqo-1313.env`, fora do repo — o deploy as lê via BuildKit secrets), `GITHUB_TOKEN` local (PAT) para os scripts GitHub, secrets no repo GitHub, `pnpm configure:branch-protection` (idempotente; reaplicar se o drift voltar), runbook manual/rollback (`docs/ops/teqo-1313-deploy.md`). +- **Só humano:** aprovar o environment `production` no run (o único gate de publicação — OPS104 nunca auto-aprova; OPS107: aprove só o run escolhido e rejeite os demais — runs não aprovados não publicam nada), envs de produção no homeserver (`~/stack/teqo-1313.env`, fora do repo — o deploy as lê via BuildKit secrets), `GITHUB_TOKEN` local (PAT) para os scripts GitHub, secrets no repo GitHub, `pnpm configure:branch-protection` (idempotente; reaplicar se o drift voltar), runbook manual/rollback (`docs/ops/teqo-1313-deploy.md`). - **Falha do `verify` (OPS106):** o deploy não fica mais parado esperando alguém notar — `auto-unblock.yml` (`workflow_run` no Deploy, só push em `main` e só job `verify` vermelho) dispara UM agente autônomo de desbloqueio no homeserver com o mesmo contrato do `worktree fix` (worktree `fix/*`, banco local `teqo_wt*`, opencode + `/bug-fix`, PR Ready + auto-merge). Single-flight: Issue token `auto-unblock` + `/tmp/teqo-unblock.lock`; falha com agente ativo só comenta no token (nunca enfileira). Guardrails fail-closed de banco/ambiente, cap de 4h, sem auto-retry do deploy e sem auto-approve de produção. Bootstrap/operação: `docs/ops/teqo-1313-deploy.md` (seção Auto-unblock) + `pnpm unblock:check`. O `AUTOMERGE_PAT` é entregue ao processo destacado (push/PR/outcome) por credential helper não-persistente — o built-in token fica só no launcher síncrono. ### Dono do PR, dono do CI @@ -78,14 +78,14 @@ Arquivos de "lista" que N agentes escrevem em paralelo (plans, migrações, cata ## CI por alvo (GitHub Actions, OPS71) -| Workflow | Trigger | Banco | Passos | -| ------------------------------------ | --------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ci-pr.yml` | PR → `main` (same-repo) | 1× service Postgres 17 | Job único `checks` sequencial (OPS62 X1): guards → lint→format→typecheck→knip→cycles→unit→int→build→e2e; skips via `ci-scope.mjs` (step `scope`); **fail-fast estrutural** — step vermelho interrompe o job, o run falha; `concurrency` nativo cancela o run em push novo; e2e **apenas blast radius** — `selected` ou, em high-risk, o conjunto `curated` (nunca `full` — OPS72/OPS86); área de risco sem mapping falha `unmapped-risk`; unit/int `changed` com seleção vazia cai em full (`vitest-changed-or-full.mjs`) — em 1 processo com `PLAYWRIGHT_WORKERS=4`; 1 service `postgres-int` (localhost:5432) com **`pnpm db:reset`** (drop→migrate→seed:minimal) entre int e build/e2e (OPS88 — substitui o 2º container); fork-PR excluída no `if:` do job (repo público — não queima minutes) | -| `deploy.yml` | **`push` em `main` (automático, OPS104) + `workflow_dispatch`** | 1× service Postgres 17 | **Cinco jobs (OPS103 + OPS104):** `preflight` (hosted; `scripts/deploy-preflight.mjs` pula o run se já há deploy `queued`/`in_progress` — `waiting` de approval não conta; dispatch manual pula o guard) → `verify` (hosted, suíte **full** sem skips — lint→format→typecheck→knip→cycles→unit→int→`db:reset`→build `.next-e2e`→e2e full; OPS88: um build só, o default saiu) → `deploy-staging` (`needs: [verify]`, `environment: staging`, `TEQO_ENV=staging`) → `requeue` (hosted, **fora** do grupo `deploy-homeserver`; `scripts/deploy-requeue.mjs` re-despacha `deploy.yml` se o head de `main` andou — OPS104) e `deploy-production` (`needs: [deploy-staging]`, `environment: production` com required reviewer, `TEQO_ENV=production`); ambos os deploys rodam em `runs-on: [self-hosted, homeserver]` e executam `scripts/deploy-homeserver.sh ` **localmente** no homeserver (sem SSH; roda até o fim com o SHA do run mesmo se `main` andar durante o verify — OPS102; idempotente; um `flock` compartilhado + `concurrency: deploy-homeserver` com `queue: max`; migrator→migrate→runner→rollout→smoke) — staging vermelho fail-closa produção; nada publica sem o approve do reviewer | -| `issue-done-on-main-merge.yml` | PR merged → `main` (same-repo) + **dispatch manual (recovery)** | — | `Closes`/`Fixes` → `done` + `in-prod` + **close** **no GitHub** (token `GITHUB_TOKEN`; body do PR via `PR_BODY` do evento GitHub — no dispatch, busca via `gh` com o mesmo contrato: merged + main + same-repo) | -| `plan-issue-ready-on-main-merge.yml` | PR merged → `main` (same-repo) + **dispatch manual (recovery)** | — | `Related #N` aguardando plano → `ready` **no GitHub** (OPS18; soft-skip; mesmo token) | -| `agent-pr-ready-automerge.yml` | PR same-repo → `main` (open/sync/…) | — | Safety net via `github-pr-automerge.mjs` (plain Node, REST+GraphQL): draft `cursor/*` → Ready (PATCH); arm `enablePullRequestAutoMerge` (rebase) **com o `AUTOMERGE_PAT`** (fail-closed — nunca o `GITHUB_TOKEN` nativo, cujo merge como `github-actions[bot]` não cria runs no `closed`; OPS71-FLIP) — o **servidor** espera o required check `CI (PR) / checks` (o OPS64 "rollup pode mentir" é estrutural aqui: job único posta um check-run honesto); draft não-`cursor/*` = veto, skip | -| `archive-cursor-agent.yml` | dispatch manual (ops) | — | Helper: cancela run + arquiva um agente Cursor Cloud (`CURSOR_API_KEY`) — **dormente (OPS65)** como o pool | +| Workflow | Trigger | Banco | Passos | +| ------------------------------------ | --------------------------------------------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ci-pr.yml` | PR → `main` (same-repo) | 1× service Postgres 17 | Job único `checks` sequencial (OPS62 X1): guards → lint→format→typecheck→knip→cycles→unit→int→build→e2e; skips via `ci-scope.mjs` (step `scope`); **fail-fast estrutural** — step vermelho interrompe o job, o run falha; `concurrency` nativo cancela o run em push novo; e2e **apenas blast radius** — `selected` ou, em high-risk, o conjunto `curated` (nunca `full` — OPS72/OPS86); área de risco sem mapping falha `unmapped-risk`; unit/int `changed` com seleção vazia cai em full (`vitest-changed-or-full.mjs`) — em 1 processo com `PLAYWRIGHT_WORKERS=4`; 1 service `postgres-int` (localhost:5432) com **`pnpm db:reset`** (drop→migrate→seed:minimal) entre int e build/e2e (OPS88 — substitui o 2º container); fork-PR excluída no `if:` do job (repo público — não queima minutes) | +| `deploy.yml` | **`push` em `main` (automático, OPS104) + `workflow_dispatch`** | 1× service Postgres 17 | **Cinco jobs (OPS103 + OPS104 + OPS107):** `preflight` (hosted; `scripts/deploy-preflight.mjs` pula o run se já há deploy `queued`/`in_progress` — `waiting` de approval não conta; dispatch manual pula o guard) → `verify` (hosted, suíte **full** sem skips — lint→format→typecheck→knip→cycles→unit→int→`db:reset`→build `.next-e2e`→e2e full; OPS88: um build só, o default saiu) → `deploy-staging` (`needs: [verify]`, `environment: staging`, `TEQO_ENV=staging`) → `requeue` (hosted, **fora** do grupo `deploy-homeserver`; `scripts/deploy-requeue.mjs` re-despacha `deploy.yml` se o head de `main` andou — OPS104) e `deploy-production` (`needs: [deploy-staging]`, `environment: production` com required reviewer, `TEQO_ENV=production`, **sem** `concurrency` desde a OPS107 — o approval não segura a lane, que vive só no staging); ambos os deploys rodam em `runs-on: [self-hosted, homeserver]` e executam `scripts/deploy-homeserver.sh ` **localmente** no homeserver (sem SSH; roda até o fim com o SHA do run mesmo se `main` andar durante o verify — OPS102; idempotente; um `flock` compartilhado é a serialização real entre runs (o `concurrency: deploy-homeserver` com `queue: max` vive só no `deploy-staging` — OPS107); migrator→migrate→runner→rollout→smoke) — staging vermelho fail-closa produção; nada publica sem o approve do reviewer (aprove só o run escolhido; rejeite os demais) | +| `issue-done-on-main-merge.yml` | PR merged → `main` (same-repo) + **dispatch manual (recovery)** | — | `Closes`/`Fixes` → `done` + `in-prod` + **close** **no GitHub** (token `GITHUB_TOKEN`; body do PR via `PR_BODY` do evento GitHub — no dispatch, busca via `gh` com o mesmo contrato: merged + main + same-repo) | +| `plan-issue-ready-on-main-merge.yml` | PR merged → `main` (same-repo) + **dispatch manual (recovery)** | — | `Related #N` aguardando plano → `ready` **no GitHub** (OPS18; soft-skip; mesmo token) | +| `agent-pr-ready-automerge.yml` | PR same-repo → `main` (open/sync/…) | — | Safety net via `github-pr-automerge.mjs` (plain Node, REST+GraphQL): draft `cursor/*` → Ready (PATCH); arm `enablePullRequestAutoMerge` (rebase) **com o `AUTOMERGE_PAT`** (fail-closed — nunca o `GITHUB_TOKEN` nativo, cujo merge como `github-actions[bot]` não cria runs no `closed`; OPS71-FLIP) — o **servidor** espera o required check `CI (PR) / checks` (o OPS64 "rollup pode mentir" é estrutural aqui: job único posta um check-run honesto); draft não-`cursor/*` = veto, skip | +| `archive-cursor-agent.yml` | dispatch manual (ops) | — | Helper: cancela run + arquiva um agente Cursor Cloud (`CURSOR_API_KEY`) — **dormente (OPS65)** como o pool | `agent-pool.yml` foi **removido no OPS65** (pool dormente; o tick `*/10` custava 144 `pnpm install`/dia na workstation). O verificador de main (`ci.yml`) foi **eliminado no OPS71** (gate 2026-08-19): só o `verify` do `deploy.yml` (iniciado por push em `main` ou dispatch manual) roda a suíte full. @@ -110,7 +110,7 @@ O flip `issue-done`/`plan-issue-ready` roda no `pull_request: closed` do PR no G Branch protection de `main` no **GitHub** (aplicada no cutover do OPS71, via `pnpm configure:branch-protection`): required check `CI (PR) / checks` (a UI exibe workflow/job; o **literal de match é `checks`** — o GitHub casa pelo nome do check-run — pin ao vivo PR #742), `strict=false`, 0 reviews, `enforce_admins: true` (nem admin mergea com CI vermelho — mesmo invariante do rule-based Forgejo). O auto-merge nativo espera esse mesmo check antes de mergear; a regra no servidor é o gate real de qualquer caminho de merge, inclusive API manual. Reaplicar/consertar drift: `pnpm configure:branch-protection` (idempotente). -Deploy (OPS53 + OPS71 + OPS103 + OPS104): **começa automático** — um `push` em `main` dispara o `deploy.yml` (o job `preflight` pula se já há deploy `queued`/`in_progress`; um run `waiting` de approval **não** conta) e `workflow_dispatch` segue como escape manual. O job `verify` (hosted, suíte full incl. e2e full, **intocado** — verifica o commit uma vez) roda **antes** dos deploys: `deploy-staging` (`environment: staging`, sem reviewer) e então `deploy-production` (`needs: [deploy-staging]`, `environment: production` com required reviewer; staging vermelho nunca deixa produção rodar). Depois do staging verde, o job hosted `requeue` compara o head de `main` com o SHA do run e re-despacha um run novo se `main` andou no meio — **fora** do grupo de concurrency, para um approval pendente não bloquear a convergência. Os dois deploys executam `scripts/deploy-homeserver.sh ` no runner self-hosted do GitHub **no homeserver** (conecta outbound ao GitHub — funciona atrás do Cloudflare tunnel; o hosted nunca toca o homeserver; o self-hosted não conta minutos hosted), parametrizado por `TEQO_ENV` (`production` default intocado; `staging` = `teqo-staging`/`teqo_staging`/`~/stack/teqo-staging.env`/`127.0.0.1:1314`). O script: flock por ambiente → guard "already deployed" (revision do container do ambiente) → build do migrator (BuildKit, secrets do env file do ambiente) → push/tag em `localhost:5000` → swap dos tags no compose (backup; sed ancorado ao serviço do ambiente) → **migrations** (`teqo-1313-migrate`/`teqo-staging-migrate` — antes do build do runner, OPS66) → build do runner (contra o banco JÁ migrado) → rollout → healthcheck → smoke. Desde a OPS102 o deploy roda até o fim com o SHA do run mesmo se `main` avançar durante o verify; a convergência é do `requeue` da OPS104 (o antigo guard de stale run segue morto); o único verde sem build é o "already deployed", que prova que o container já roda o SHA. Falha = job vermelho; nada é publicado pela metade. Runbook (rollback, falhas conhecidas, bootstrap do staging): `docs/ops/teqo-1313-deploy.md`. O deploy Vercel foi removido no OPS50; o verificador automático de main (`ci.yml`) foi eliminado no OPS71 — a rede de regressão de `main` agora é o `verify` full do próprio `deploy.yml`, que nasce no merge (OPS104 supersede o anti-goal "manual-only" da OPS102: automatiza-se o **início** do run, nunca o approve de produção). +Deploy (OPS53 + OPS71 + OPS103 + OPS104 + OPS107): **começa automático** — um `push` em `main` dispara o `deploy.yml` (o job `preflight` pula se já há deploy `queued`/`in_progress`; um run `waiting` de approval **não** conta) e `workflow_dispatch` segue como escape manual. O job `verify` (hosted, suíte full incl. e2e full, **intocado** — verifica o commit uma vez) roda **antes** dos deploys: `deploy-staging` (`environment: staging`, sem reviewer) e então `deploy-production` (`needs: [deploy-staging]`, `environment: production` com required reviewer; staging vermelho nunca deixa produção rodar). Desde a OPS107 o `deploy-production` não tem `concurrency`: o approval pendente não segura o staging nem a aprovação de runs novos — o grupo vive só no `deploy-staging` (aprove só o run escolhido e rejeite os demais). Depois do staging verde, o job hosted `requeue` compara o head de `main` com o SHA do run e re-despacha um run novo se `main` andou no meio — **fora** do grupo de concurrency, para um approval pendente não bloquear a convergência. Os dois deploys executam `scripts/deploy-homeserver.sh ` no runner self-hosted do GitHub **no homeserver** (conecta outbound ao GitHub — funciona atrás do Cloudflare tunnel; o hosted nunca toca o homeserver; o self-hosted não conta minutos hosted), parametrizado por `TEQO_ENV` (`production` default intocado; `staging` = `teqo-staging`/`teqo_staging`/`~/stack/teqo-staging.env`/`127.0.0.1:1314`). O script: flock único no host (compose/workspace/registry são compartilhados) → guard "already deployed" (revision do container do ambiente) → build do migrator (BuildKit, secrets do env file do ambiente) → push/tag em `localhost:5000` → swap dos tags no compose (backup; sed ancorado ao serviço do ambiente) → **migrations** (`teqo-1313-migrate`/`teqo-staging-migrate` — antes do build do runner, OPS66) → build do runner (contra o banco JÁ migrado) → rollout → healthcheck → smoke. Desde a OPS102 o deploy roda até o fim com o SHA do run mesmo se `main` avançar durante o verify; a convergência é do `requeue` da OPS104 (o antigo guard de stale run segue morto); o único verde sem build é o "already deployed", que prova que o container já roda o SHA. Falha = job vermelho; nada é publicado pela metade. Runbook (rollback, falhas conhecidas, bootstrap do staging): `docs/ops/teqo-1313-deploy.md`. O deploy Vercel foi removido no OPS50; o verificador automático de main (`ci.yml`) foi eliminado no OPS71 — a rede de regressão de `main` agora é o `verify` full do próprio `deploy.yml`, que nasce no merge (OPS104 supersede o anti-goal "manual-only" da OPS102: automatiza-se o **início** do run, nunca o approve de produção). ## Cursor Cloud diff --git a/docs/changelog/2026-09-14-ops107.md b/docs/changelog/2026-09-14-ops107.md new file mode 100644 index 00000000..641333db --- /dev/null +++ b/docs/changelog/2026-09-14-ops107.md @@ -0,0 +1 @@ +**OPS107 — Aprovar e deployar em produção a versão escolhida, sem fila de pendentes (2026-09-14):** o approval de produção deixou de segurar a lane compartilhada — com N runs empilhados, o operador aprova e publica **qualquer** run (tipicamente o último com staging verde) sem tocar nos anteriores. O bloco `concurrency: deploy-homeserver` (`cancel-in-progress: false`, `queue: max`) foi removido do job `deploy-production` e vive **só** no `deploy-staging` (que nunca espera approval e sempre drena); o job `waiting` de produção segurava o grupo porque a avaliação de concurrency acontece antes do gate do environment, travando o staging e a aprovação dos runs novos (evidência ao vivo 2026-09-14: run 34818635060 waiting de 08:22 a 10:12, runs 34822356060/34824353858 com staging preso desde 08:38/09:06 — o gatilho de revisita da falha conhecida do runbook, disparado). A serialização real permanece intocada: cadeia `needs` intra-run + UMA instância do runner self-hosted (um job por vez) + o `flock` único do `deploy-homeserver.sh` (comentário realinhado; nenhuma linha executável mudou). Nada de auto-approve/auto-cancel/auto-retry: produção segue exclusivamente humana (`environment: production` com required reviewer, `verify` full verde) e runs não aprovados não publicam nada; multi-approve segue "o último deploy executado vence" — o runbook registra **"aprove só o run escolhido; rejeite os demais"**. Contrato pinado em `ciSkipInvariants.unit.spec.ts`: o grupo é exigido **no bloco do `deploy-staging`** e `concurrency:` é proibido **no bloco do `deploy-production`** (RED comprovado contra o YAML anterior); `deployTrigger`/`deployScript` intocados e verdes. Docs realinhadas: runbook `docs/ops/teqo-1313-deploy.md` (intro, fluxo itens 1/5/7, primeiro deploy e a falha conhecida agora resolvida), `AGENTS.md`, `AGENTS-infra.md`, `docs/AGENT-OPS.md` e `README.md`. Sem migration, sem UI, sem access/Consent. Verificação: `pnpm gate:fast` verde (310 arquivos/3062 unit); e2e não afetado (diff YAML/teste unit/docs — o CI do PR roda o curado por high-risk); aceite ao vivo pós-merge: deixar um run `waiting`, confirmar o staging de um run novo rodando e aprovar só o escolhido. diff --git a/docs/ops/teqo-1313-deploy.md b/docs/ops/teqo-1313-deploy.md index fea5a93a..164f4726 100644 --- a/docs/ops/teqo-1313-deploy.md +++ b/docs/ops/teqo-1313-deploy.md @@ -1,4 +1,4 @@ -# Runbook: deploy do site 1313 no homeserver (OPS53 + OPS71 + OPS103 + OPS104 + OPS106 — início automático, produção com review) +# Runbook: deploy do site 1313 no homeserver (OPS53 + OPS71 + OPS103 + OPS104 + OPS106 + OPS107 — 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 @@ -9,13 +9,20 @@ 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). +Desde a OPS107 o approval de produção **não ocupa a lane compartilhada** +(`concurrency` só no `deploy-staging`): runs novos seguem fazendo verify e +staging e ficam aprováveis independentemente — o operador aprova **só o run +escolhido** (tipicamente o último com staging verde) e rejeita os demais. + ## 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 → + pendentes (o humano aprova qual promover — e o staging do run novo **não** + espera: desde a OPS107 o approval pendente não segura o grupo). + `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 @@ -34,8 +41,17 @@ roda a suíte **full** (incl. e2e full) **uma vez** e gateia os dois alvos: `environment: production`) **aguarda a aprovação do reviewer** e então executa `TEQO_ENV=production bash scripts/deploy-homeserver.sh ` — 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`. + Staging vermelho = produção nunca roda. O job de produção **não tem** + `concurrency` desde a OPS107: a avaliação de concurrency acontece antes do + gate do environment e um run `waiting` segurava o grupo `deploy-homeserver`, + travando o staging (e a aprovação) dos runs novos. O grupo vive **só** no + `deploy-staging`, que não espera approval (só a vez no próprio grupo) e + sempre drena; a execução + entre runs é serializada pela cadeia `needs`, pelo runner self-hosted + único e pelo `flock` do host. **Operação:** aprove **só o run escolhido** + (tipicamente o último com staging verde) e **rejeite os demais** — runs + não aprovados não publicam nada; se dois forem aprovados, o último deploy + executado vence. 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 @@ -43,8 +59,9 @@ roda a suíte **full** (incl. e2e full) **uma vez** e gateia os dois alvos: 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 + são compartilhados — um deploy por vez no host; desde a OPS107 o + `concurrency` do workflow só existe no staging, então o lock ÚNICO é a + serialização real entre runs e 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) → @@ -80,7 +97,9 @@ teqo-` → healthcheck → smoke (`/`, `/campanha/login`, `/admin`, **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 +aguardando aprovação. Para validar a OPS107, deixe esse run `waiting`, dispare +um segundo (merge/dispatch) e confirme que o `deploy-staging` dele roda com o +grupo livre; aprove só o escolhido e rejeite o outro. 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, @@ -253,7 +272,7 @@ a correção se re-mergeia e o próximo deploy completo publica. | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 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:-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 aprovação pendente de produção: staging do run novo ficava `pending` (resolvido) | O job `waiting` de approval **segurava o grupo** `deploy-homeserver` (a avaliação de concurrency acontece antes do approval — comportamento do GitHub) | **Resolvido na OPS107:** o `concurrency` vive só no `deploy-staging` (que nunca espera approval); o `deploy-production` não ocupa lane, então runs novos fazem staging e ficam aprováveis. Aprove só o run escolhido e rejeite os demais — runs não aprovados não publicam; multi-approve = o último deploy executado vence (rollback manual se um SHA indesejado publicar) | | 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 | diff --git a/docs/plans/ops107-aprovacao-producao-escolhida-impl.md b/docs/plans/ops107-aprovacao-producao-escolhida-impl.md new file mode 100644 index 00000000..6208e8bd --- /dev/null +++ b/docs/plans/ops107-aprovacao-producao-escolhida-impl.md @@ -0,0 +1,132 @@ +# Impl: OPS107 — Aprovar e deployar em produção a versão escolhida, sem fila de pendentes + +Status: aprovado (gate humano 2026-09-14) +Atualizado em: 2026-09-14 +Issue: #1002 +Intenção: docs/plans/ops107-aprovacao-producao-escolhida.md +Appetite restante: herdado (~0,5–1 dia eng); sem ajuste — remoção de 4 linhas do YAML, 1 pin endurecido, docs e changelog + +## Leitura da intenção + +- **Outcome (nas palavras do aceite):** com N runs empilhados, o operador aprova e publica **qualquer** um — tipicamente o último com staging verde — sem tocar nos anteriores; um run aguardando aprovação **não** bloqueia o staging nem a aprovação de runs novos; aprovar o run escolhido publica o SHA dele e os antigos não aprovados não deployam nada; a publicação continua exclusivamente humana, com `verify` full verde e nada auto-aprovado/cancelado/atalhado. +- **O que NÃO negociar:** nunca auto-aprovar/publicar produção; nunca atalhar o `verify` full; nunca workflow gêmeo; nunca auto-cancelar/supersedir runs antigos; `environment: production` com required reviewer é o único gate de publicação; `scripts/deploy-homeserver.sh` sem mudança executável (`flock` único + guard "already deployed" não podem afrouxar — pin `deployScript.unit.spec.ts`); `verify`, `ci-pr.yml`, guards de banco e proteção de branch fora de escopo; rollback por SHA arbitrário segue manual no homeserver. +- **O que reavaliar (hipóteses da intenção/explorador):** + - A OPS104 registrou o grupo compartilhado como decisão OPS103/intenção e o capturou como risco R2 ("revisitar se aprovações demorarem sistematicamente"). **O gatilho disparou:** a premissa "grupo intocado" está superseded — o posicionamento do grupo é exatamente o que muda. A reavaliação confirma que a serialização real não depende dele em produção (cadeia `needs` + runner único + `flock`). + - Hipótese "o pin do `ciSkipInvariants` continua verde com a remoção" — **confirmada** (pina por `toContain` global). É justamente por isso que o pin precisa ser endurecido; sem isso a entrega não tem prova. + - Hipótese "precisa mexer nos scripts de trigger (`deploy-preflight.mjs`/`deploy-trigger.mjs`)" — **falsa**: `waiting` já não conta como ativo (pin `deployTrigger.unit.spec.ts` l.57–64) e, sem o grupo em produção, um `waiting` não segura nada. Nenhum script de trigger muda. + - Hipótese "o `deploy-homeserver.sh` precisa de lock por ambiente" — **falsa/rejeitada**: o lock único continua sendo a serialização real (compose/workspace/registry são compartilhados). + +## Abordagem recomendada + +```mermaid +flowchart LR + M["merge em main
(N runs empilhados)"] --> V["verify full (hosted)
um por run"] + V --> S["deploy-staging
grupo deploy-homeserver
nunca espera approval"] + S -->|"staging verde"| W["deploy-production
environment: production
aguarda reviewer — SEM grupo"] + S --> Q["requeue (hosted)
fora do grupo"] + Q -.->|"main andou"| M + W -->|"approve do run escolhido"| P["script no runner self-hosted
(1 job por vez)"] + W -.->|"runs não aprovados"| N["ficam waiting — nada publica
(rejeitar na UI)"] + P --> F["flock único no host:
serializa staging×produção e runs entre si"] +``` + +**Opções consideradas:** A) remover o `concurrency` de `deploy-production` e manter `group: deploy-homeserver` só em `deploy-staging`; A2) remover o `concurrency` dos dois jobs (fiar na cadeia `needs` + runner único + `flock`); B) manter em produção um grupo único por run (`${{ github.run_id }}`); C) manter o grupo em produção e removê-lo do staging; D) produção por `workflow_dispatch` manual com SHA; E) auto-cancelar/supersedir runs antigos. +**Recomendação:** **A** — o job `waiting` só existe onde há approval (produção); desacoplar o approval da lane compartilhada elimina o bloqueio na origem. O staging (única lane que nunca espera humano, sempre drena) mantém o grupo com `queue: max`/`cancel-in-progress: false`, preservando o contrato OPS102/OPS103 de "deploy nunca cancela, espera a vez" para o caminho sem humano. A serialização real fica em três camadas: cadeia `needs` intra-run, UMA instância de runner self-hosted (um job por vez) e o `flock` único do script (também cobre invocação manual e um futuro scale de runner). Produção segue 100% humana (`environment: production` + required reviewer) e o `verify` full segue gate. +**Rejeitadas:** A2 porque remove sem necessidade a serialização declarativa da lane de staging (um segundo runner passaria a buildar staging concorrentemente, dependendo só do `flock`) e amplia o delta sem ganho de aceite; B porque `${{ github.run_id }}` cria um grupo que ninguém mais compartilha — no-op cerimonial que não serializa nada entre runs e só adiciona pin; C porque mantém o approval segurando a lane e a produção aprovada do run novo entra na fila atrás do `waiting` antigo (piora o problema central); D porque foi rejeitada no gate de produto (mecanismo novo de promoção por SHA e perda da cadeia `needs`/verify do run); E porque auto-cancelamento/supersessão é anti-goal explícito (automação destrutiva sem decisão de produto). + +### Decisões de engenharia (caras de reverter) + +#### D1 — Onde mora o `concurrency` depois da OPS107 + +- **Opções:** A) produção fora do grupo, grupo só no staging (recomendada); A2) sem grupo em nenhum job; B) grupo único por run em produção (`${{ github.run_id }}`); C) grupo só em produção. +- **Recomendação:** **A** — a lane que pode ficar `waiting` (produção) não ocupa nada; a lane que sempre drena (staging) mantém o grupo e o `queue: max`, que é o contrato OPS102/103 ("deploys esperam a vez, nunca são cancelados") para o caminho que não depende de humano. +- **Rejeitadas:** A2 por D1/opções (perde a serialização declarativa do staging num eventual segundo runner); B por ser grupo sem consumidor; C por reproduzir o bug central (produção nova atrás do `waiting` antigo). + +#### D2 — O que é a serialização real (e o que o plano NÃO cria) + +- **Opções:** A) grupos/locks por ambiente (staging e produção separados); B) mecanismo novo de coordenação (token/lock no GitHub, revogar approval etc.); C) confiar na cadeia `needs` + runner único + `flock` único existente, documentando (recomendada). +- **Recomendação:** **C** — `needs` serializa intra-run (produção nunca começa antes do staging verde); UMA instância de runner self-hosted (`~/actions-runner`, labels `self-hosted,homeserver`) roda um job por vez; o `flock -w 3600` em `/tmp/teqo-deploy.lock` (compartilhado pelos dois ambientes; pinado em `deployScript.unit.spec.ts`) serializa staging×produção entre runs e a invocação manual. Zero abstração nova. +- **Rejeitadas:** A porque compose/workspace/registry são um só — locks por ambiente deixariam staging e produção buildar/swapar concorrentemente (decisão OPS103, reafirmada); B porque o gate de produto decidiu não introduzir mecanismo novo de trava e o problema é de posicionamento do job, não de coordenação. + +#### D3 — Multi-aprovação (dois runs aprovados por engano ou de propósito) + +- **Opções:** A) manter a ordem de aprovação/execução (o último deploy executado vence) + runbook "aprove só o escolhido; rejeite os demais" (recomendada, decidida no gate); B) mecanismo novo para "desarmar" approvals antigos; C) auto-cancelar runs superseded. +- **Recomendação:** **A** — a semântica é a mesma de hoje (o `flock` serializa mas não ordena; o "already deployed" não pega revision diferente); os runs não aprovados ficam `waiting` e podem ser rejeitados na UI sem publicar. +- **Rejeitadas:** B porque não há API limpa de revogação de approval e o gate rejeitou mecanismo novo de trava; C porque cancelamento automático é anti-goal. + +#### D4 — Pin do contrato (o teste que prova o aceite) + +- **Opções:** A) fatiar o YAML por bloco de job e pinar presença do grupo no staging + ausência de `concurrency:` em produção (recomendada); B) manter os `toContain` globais atuais; C) pinar ausência global de `concurrency:` no arquivo. +- **Recomendação:** **A** — o pin atual continuaria verde após a remoção (não prova nada); o fatiamento já é o padrão do próprio spec (bloco do `requeue`, l.142–147) e `readWorkflow` remove comentários, então a prosa do header nunca satisfaz o pin. RED comprovado antes da edição do YAML (o `not.toContain` falha contra o YAML antigo) e verde depois. +- **Rejeitadas:** B por ser falso-verde para a mudança central; C porque quebraria o grupo legítimo do staging. + +**Nota (barata, sem decisão):** realinhar os comentários que descrevem a serialização — header do `deploy.yml` (l.41–49), comentário do job `deploy-production` e o comentário de lock do `scripts/deploy-homeserver.sh` (l.50–54, **somente comentário**; nenhuma linha executável muda e o pin do script continua verde sem edição). Sem isso, o comentário dentro do script passaria a mentir sobre onde a serialização mora. + +### Componentes / mudanças + +Linhas do estado atual (2026-09-14); realinhar pela âncora, não pelo número. + +- **`deploy-production`** (`.github/workflows/deploy.yml` l.251–276): **remover o bloco `concurrency:` (l.267–270)**. Permanecem `needs: [deploy-staging]`, o `if` de ref guard, `environment: production`, `runs-on: [self-hosted, homeserver]`, `timeout-minutes: 60`, `TEQO_ENV: production` e o step `bash scripts/deploy-homeserver.sh "$GITHUB_SHA"`. Comentário do job ganha a razão (OPS107: o approval é avaliado antes do environment gate; um `waiting` nunca pode ocupar a lane compartilhada). +- **`deploy-staging`** (idem l.202–227): **intocado** — `needs: [verify]`, `environment: staging`, `runs-on: [self-hosted, homeserver]`, `concurrency: {group: deploy-homeserver, cancel-in-progress: false, queue: max}`. É a única lane que segura o grupo e sempre drena. +- **Header do workflow** (idem l.41–49) e sumário de jobs (l.17–32): reescrever o parágrafo de serialização — o grupo vive **só** no staging; produção serializa por `needs` + runner único + `flock`; um approval pendente não ocupa lane. O bloco OPS104 (l.55–58, "no auto-approve, no auto-retry, no cancel-old-runs") permanece verdadeiro. +- **`scripts/deploy-homeserver.sh`** (l.50–54): **somente comentário** — o lock compartilhado segue a serialização real; nada de comportamento muda; `deployScript.unit.spec.ts` sem edição e verde. +- **`ciSkipInvariants`** (`tests/unit/ciSkipInvariants.unit.spec.ts` l.156–178): endurecer o pin. Manter os asserts globais atuais (`environment: staging|production`, `needs: [deploy-staging]`, `TEQO_ENV`, ref guard, `needs.deploy-staging.result`) e **escopar o `concurrency` por fatia de bloco**: `stagingBlock = deploy.slice(indexOf(' deploy-staging:'), indexOf(' requeue:'))` contém `concurrency:`, `group: deploy-homeserver`, `cancel-in-progress: false`, `queue: max`; `productionBlock = deploy.slice(indexOf(' deploy-production:'))` (último job, até o EOF) **não** contém `concurrency:`. O pin do `requeue` fora do grupo (l.140–147) fica como está. +- **`deployTrigger.unit.spec.ts`**: sem mudança — o pin de `waiting` não-ativo (l.57–64) continua o contrato do `preflight`. +- **`deployScript.unit.spec.ts`**: sem mudança — sem mudança executável no script. +- **Runbook** (`docs/ops/teqo-1313-deploy.md`): título (l.1) + intro (l.3–10) ganham OPS107; item 1 (l.16–18) registra que o staging do run novo segue mesmo com approval pendente; item 5 (l.33–38) troca "serializada pela `concurrency: deploy-homeserver`" por "serialização real = cadeia `needs` + runner único + `flock` único; o approval pendente não ocupa lane" e adiciona a instrução **"aprove só o run escolhido; rejeite os demais"**; item 7 (l.45–48) troca "o `concurrency` do workflow serializa os runs" por "o grupo fica só no staging; o lock único serializa os runs entre si e cobre invocação manual"; "Primeiro deploy" (l.80–87) descreve o fluxo novo; "Falhas conhecidas" l.256 é reescrita: o sintoma antigo deixa de existir e a linha passa a registrar a multi-aprovação (último deploy executado vence; rejeitar os demais; rollback manual se um SHA indesejado publicar). +- **`AGENTS.md`** (l.15), **`AGENTS-infra.md`** (l.5), **`docs/AGENT-OPS.md`** (l.19, l.25, l.84, l.113), **`README.md`** (l.12 e l.100): realinhar as frases de deploy — `concurrency: deploy-homeserver` só no `deploy-staging`; approval pendente não bloqueia staging nem aprovação de runs novos (OPS107); produção serializa por runner único + `flock`; "só humano: aprovar o run escolhido no environment `production` (rejeitar os demais)". +- **Changelog**: `docs/changelog/2026-09-14-ops107.md` (uma entrada, one-liner bold, additions-only) registrando: grupo só no staging; approval de produção não ocupa mais a lane; serialização real = `needs` + runner único + `flock`; resolução do R2/OPS104 e supersessão do "grupo intocado"; instrução multi-approve. **Não** editar `docs/CHANGELOG-AGENTS.md` (gitignored) nem `docs/CHANGELOG-AGENTS-HISTORY.md` (congelado). +- **Migration:** sem migration (YAML/testes/docs). +- **Access / Consent:** n/a (sem PII, sem collection). +- **UI:** Impeccable A — n/a (sem superfície de UI). + +### Dados → forma (se aplicável) — n/a + +A "forma" aqui é operacional (jobs/runs no Actions): a evidência é o estado dos runs (`pending`/`waiting`, ordem de liberação da lane) e a revision do container publicado. Nada a apresentar. + +## Fases verificáveis + +1. **Tracer — workflow + pin (RED → verde).** Primeiro endurecer `ciSkipInvariants` (fase D4) e rodar o spec focado: a asserção `productionBlock.not.toContain('concurrency:')` falha contra o YAML atual (**RED comprovado**). Depois remover o bloco `concurrency:` de `deploy-production` e realinhar comentários do YAML. Gate: spec focado verde + `pnpm gate:fast`. _~2–3h._ +2. **Docs vivas + runbook + changelog.** Checklist da seção Componentes (runbook, AGENTS/AGENTS-infra/AGENT-OPS/README, entrada de changelog; `pnpm format` se o Prettier reclamar). _~2–3h._ +3. **Gates e entrega.** `pnpm gate:fast`, `pnpm test:unit` (full), `pnpm push` (o `gate:ci` cobre format/lint/typecheck/unit/int/knip/cycles); e2e: **sem superfície de runtime — declarar "sem e2e afetado"** (diff é YAML + spec unitário + docs; politica OPS72 discricionária). O CI do PR roda o curado automaticamente porque `tests/unit/ciSkipInvariants.unit.spec.ts` é `HIGH_RISK_EXACT` (nunca zero e2e), além de unit/int full. Incluir o `*-impl.md` no commit; PR Ready. _~1h + observação pós-merge:_ (a) o merge dogfooda o YAML novo — deixar o `deploy-production` do próprio run **waiting**; (b) mergear/dispatchar um segundo run e confirmar que o `deploy-staging` dele **roda** (grupo livre) — aceite central; (c) aprovar **só** o run escolhido e conferir a revision do container `teqo-1313` no SHA dele; (d) rejeitar (ou deixar waiting) o run antigo e confirmar que ele não publicou nada. + +## Rabbit holes / Não escopo (engenharia) + +- Auto-approve/auto-publicação, auto-retry de `verify` vermelho, auto-cancelamento/supersessão de runs antigos (anti-goals). +- Produção por `workflow_dispatch` manual com SHA, novo "promoter"/coordinator, qualquer script ou lock novos — o dono continua sendo o `deploy.yml` (editar o dono, não criar twin). +- Locks/grupos por ambiente no `deploy-homeserver.sh` — decisão OPS103 reafirmada (compose/workspace/registry compartilhados). +- Mexer no `verify`, no `ci-pr.yml`, nos guards de banco, na proteção de branch ou no `environment` de produção. +- Refinar o `preflight` para contar `waiting` — contra o aceite (um waiting não pode bloquear runs novos) e já pinado. +- Reintroduzir o stale-run guard (`git ls-remote`/`refs/heads/main`) — superseded pela OPS102. +- actionlint/validação de YAML nova no CI; e2e novo (não há superfície de runtime); rollback por SHA arbitrário (manual no homeserver). + +**Diferido com gatilho (triage do simplify):** o slice do `productionBlock` no pin vai até o EOF do YAML (direção fail-closed — um job novo depois de `deploy-production` com `concurrency:` geraria falso-RED, nunca falso-verde). Gatilho: ao adicionar qualquer job após `deploy-production` no `deploy.yml`, delimitar o bloco no início do próximo job. Descartado da triage: ajuste editorial do self-score (sem impacto funcional/rastreabilidade). + +## Riscos e mitigação + +- **R1 — Multi-aprovação acidental com execução fora de ordem** (o `flock` serializa mas não ordena). Risco aceito no gate. _Mitigação:_ runbook "aprove só o escolhido; rejeite os demais"; rollback manual documentado; o "already deployed" evita rebuild do mesmo SHA. +- **R2 — Confiar no runner único + `flock` como serialização real.** Documentar no runbook/comentários que escalar o runner exige revisitar (o grupo GitHub deixa de ordenar produção; o `flock` continua cobrindo a concorrência no host). _Gatilho:_ segundo runner instalado. +- **R3 — Produção aprovada rodando no runner enquanto o staging de um run novo espera o runner.** Aceitável (ambientes separados; um job por vez + `flock`) — documentar na intro/header. +- **R4 — Pin frágil.** _Mitigação:_ fatia por bloco (não `contains` global), RED comprovado antes; `requeue`/`deployTrigger`/`deployScript` pins preservados. +- **R5 — Drift das docs vivas** (runbook + 4 arquivos + changelog). _Mitigação:_ checklist da fase 2; changelog registra a supersessão. +- **R6 — YAML inválido derruba os dois caminhos** (auto e dispatch usam a default branch). _Mitigação:_ diff pequeno (4 linhas), revisão; o merge é o dogfood e `workflow_dispatch` segue como escape. +- **R7 — Runs antigos ficam `waiting` para sempre na UI** (sem automação). Aceito; o runbook orienta rejeitar. Nenhuma publicação ocorre sem approve. +- **R8 — Backlog serial de staging** (N merges = N stagings, cada um ~15–20 min). Aceito: staging drena sem humano no caminho (era o bloqueio que doía). _Gatilho:_ se a fila virar sistemática, reavaliar cancelamento automático — exige decisão de produto. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção coberto: (1) com N runs empilhados, aprovar/publicar qualquer um sem tocar nos anteriores; (2) run `waiting` não bloqueia o staging nem a aprovação de runs novos (grupo fora de produção); (3) aprovar o run escolhido publica o SHA dele; antigos não aprovados não publicam; (4) publicação exclusivamente humana, `verify` full verde, nada auto-aprovado/auto-cancelado/atalhado. +- [ ] Invariantes AGENTS/engineering-standards: sem migration/UI/access/Consent; `verify`/`ci-pr.yml`/guards/proteção de branch intocados; `environment: production` com required reviewer é o único gate de publicação; `scripts/deploy-homeserver.sh` sem mudança executável (flock + guard intactos; `deployScript.unit.spec.ts` verde sem edição); sem workflow gêmeo, sem auto-cancel/auto-approve. +- [ ] Testes de domínio previstos: `ciSkipInvariants.unit.spec.ts` endurecido (grupo presente no bloco do staging; `concurrency:` ausente no bloco de produção) com RED comprovado contra o YAML antigo; `deployTrigger.unit.spec.ts` e `deployScript.unit.spec.ts` verdes sem edição. +- [ ] Gates: `pnpm gate:fast` e `pnpm test:unit` verdes; `pnpm push` (gate:ci); e2e declarado "sem superfície" (OPS72) — o CI do PR aplica o curado por high-risk; validação ao vivo pós-merge (fase 3) executada e registrada no changelog/runbook. + +**Self-score decision-quality (gate ≥4):** + +| Critério | Nota | Justificativa | +| -------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| 1. Decisões caras com rejeitadas | 1,0 | D1 (A/A2/B/C), D2 (locks por ambiente vs mecanismo novo vs reuso), D3 (multi-approve) e D4 (pin) com opções e rejeitadas explícitas | +| 2. Cabe no appetite | 1,0 | <1 dia: 4 linhas removidas do YAML + comentários, 1 pin endurecido, docs/changelog; nenhuma refatoração do fluxo | +| 3. Rabbit holes nomeados | 1,0 | auto-approve/auto-cancel, dispatch manual por SHA, locks por ambiente, refinamento do preflight, coordinator novo, actionlint | +| 4. Depth check (reuso) | 1,0 | edita o dono (`deploy.yml`), reusa o precedente do `requeue` e o padrão de slice do próprio spec; zero abstração/dependência nova | +| 5. Intenção preservada | 1,0 | os 4 aceites cobertos; publicação 100% humana; `verify` e o script do homeserver sem mudança de comportamento | +| **Total** | **5,0/5** | ≥4 exigido | diff --git a/scripts/deploy-homeserver.sh b/scripts/deploy-homeserver.sh index bbd6fa7f..c76a3c9f 100644 --- a/scripts/deploy-homeserver.sh +++ b/scripts/deploy-homeserver.sh @@ -49,8 +49,10 @@ WORKSPACE_DIR="${WORKSPACE_DIR:-$HOME/teqo-deploy}" TEQO_REGISTRY="${TEQO_REGISTRY:-localhost:5000}" # ONE lock for both environments: the compose file, the workspace and the # local registry are shared, so staging and production must never build or -# swap concurrently (the workflow's job-level concurrency serializes the -# runs; this lock also covers manual invocations on the host). +# swap concurrently (the workflow's job-level concurrency lives only on +# deploy-staging since OPS107 — the production approval never holds it, so +# this lock IS the cross-run serialization; it also covers manual invocations +# on the host). DEPLOY_LOCK="${DEPLOY_LOCK:-/tmp/teqo-deploy.lock}" say() { printf '[deploy] %s\n' "$*"; } diff --git a/tests/unit/ciSkipInvariants.unit.spec.ts b/tests/unit/ciSkipInvariants.unit.spec.ts index 2bb733c3..1ad35546 100644 --- a/tests/unit/ciSkipInvariants.unit.spec.ts +++ b/tests/unit/ciSkipInvariants.unit.spec.ts @@ -153,7 +153,7 @@ describe('ciSkipInvariants', () => { expect(ciPr).not.toContain('ci-classify-production.mjs') }) - it('deploy.yml chains verify → deploy-staging → deploy-production with separate environments (OPS103)', () => { + it('deploy.yml chains verify → deploy-staging → deploy-production with separate environments (OPS103 + OPS107)', () => { // One dispatch, one verify, two separately approved deploys. The gate is // the GitHub Environment (reviewer on production, configured in the repo — // not in this YAML); the chain is needs-based so a red staging fail-closes @@ -169,12 +169,24 @@ describe('ciSkipInvariants', () => { expect(deploy).toContain(' TEQO_ENV: production') expect(deploy).toContain("github.ref == 'refs/heads/main'") expect(deploy).toContain("needs.deploy-staging.result == 'success'") - // Deploys share the homeserver compose/workspace: serialize them across - // runs without cancelling an in-flight deploy, and queue (never cancel) a - // previously pending one (`queue: max`). - expect(deploy).toContain(' group: deploy-homeserver') - expect(deploy).toContain(' cancel-in-progress: false') - expect(deploy).toContain(' queue: max') + // OPS107: only staging holds the shared lane. Staging never waits on an + // approval, so it always drains; the production approval must NOT hold a + // concurrency group — a `waiting` job blocks every later run's staging + // (the bug this delivers). Real serialization: the needs chain inside a + // run, the single self-hosted runner and the host `flock`. + const stagingBlock = deploy.slice( + deploy.indexOf(' deploy-staging:'), + deploy.indexOf(' requeue:'), + ) + expect(stagingBlock).toContain('concurrency:') + expect(stagingBlock).toContain(' group: deploy-homeserver') + expect(stagingBlock).toContain(' cancel-in-progress: false') + expect(stagingBlock).toContain(' queue: max') + const productionBlock = deploy.slice(deploy.indexOf(' deploy-production:')) + expect(productionBlock).not.toContain('concurrency:') + // A workflow-level `concurrency:` would be acquired when the run starts + // (before any approval) and reintroduce the same blockage. + expect(deploy).not.toMatch(/^concurrency:/m) }) it('every campaign domain dir is covered by the e2e affected manifest', () => { diff --git a/tests/unit/deployScript.unit.spec.ts b/tests/unit/deployScript.unit.spec.ts index b4c8fdc9..b1012da1 100644 --- a/tests/unit/deployScript.unit.spec.ts +++ b/tests/unit/deployScript.unit.spec.ts @@ -207,8 +207,9 @@ describe('scripts/deploy-homeserver.sh environment parameterization (OPS103)', ( it('serializes both environments on ONE shared lock (compose/workspace are shared)', () => { // Per-env locks would let staging and production build/swap concurrently - // on the same workspace and compose file (the workflow concurrency does - // not cover manual invocations on the host). + // on the same workspace and compose file (the workflow job-level + // concurrency lives only on deploy-staging since OPS107 — this lock also + // covers cross-run execution and manual invocations on the host). expect(script).toContain('DEPLOY_LOCK="${DEPLOY_LOCK:-/tmp/teqo-deploy.lock}"') expect(script).not.toContain('/tmp/teqo-1313-deploy.lock') expect(script).not.toContain('/tmp/teqo-staging-deploy.lock')