fix(OPS107): aprovação de produção não ocupa a lane do staging (Closes #1002)
Some checks are pending
Deploy / preflight (push) Waiting to run
Deploy / verify (push) Blocked by required conditions
Deploy / deploy staging (teqo-staging) (push) Blocked by required conditions
Deploy / requeue when main advanced (push) Blocked by required conditions
Deploy / deploy production (teqo-1313) (push) Blocked by required conditions

This commit is contained in:
Francisco Solla 2026-09-14 08:01:59 -03:00
parent 8fe3331687
commit 119b8980fb
11 changed files with 229 additions and 54 deletions

View file

@ -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:

View file

@ -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`.

File diff suppressed because one or more lines are too long

View file

@ -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

View file

@ -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
@ -79,9 +79,9 @@ 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 <sha>` **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 |
| `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 <sha>` **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 |
@ -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 <sha>` 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 <sha>` 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

View file

@ -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.

View file

@ -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 <sha>` —
publica o **mesmo SHA** em produção (`teqo-1313`, `127.0.0.1:1313`).
Staging vermelho = produção nunca roda; a corrida entre os dois é
serializada pela `concurrency: deploy-homeserver`.
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-<env>` → 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:<sha>-rebuild`) e `docker compose up -d`, ou espere um commit novo |
| Merge durante aprovação pendente de produção: staging do run novo fica `pending` | O job `waiting` de approval **segura o grupo** `deploy-homeserver` (a avaliação de concurrency acontece antes do approval — comportamento do GitHub) | O run novo já nasceu (o guard não conta `waiting`); aprovar/rejeitar o run pendente libera o grupo. Nada publica sem reviewer. Revisitar se aprovações passarem a demorar sistematicamente → avaliar grupos/locks por ambiente (o lock compartilhado é decisão da OPS103) |
| Merge durante 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 |

View file

@ -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<br/>(N runs empilhados)"] --> V["verify full (hosted)<br/>um por run"]
V --> S["deploy-staging<br/>grupo deploy-homeserver<br/>nunca espera approval"]
S -->|"staging verde"| W["deploy-production<br/>environment: production<br/>aguarda reviewer — SEM grupo"]
S --> Q["requeue (hosted)<br/>fora do grupo"]
Q -.->|"main andou"| M
W -->|"approve do run escolhido"| P["script no runner self-hosted<br/>(1 job por vez)"]
W -.->|"runs não aprovados"| N["ficam waiting — nada publica<br/>(rejeitar na UI)"]
P --> F["flock único no host:<br/>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 |

View file

@ -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' "$*"; }

View file

@ -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', () => {

View file

@ -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')