mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
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
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:
parent
8fe3331687
commit
119b8980fb
11 changed files with 229 additions and 54 deletions
46
.github/workflows/deploy.yml
vendored
46
.github/workflows/deploy.yml
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
# Deploy (OPS71 — GitHub Actions; staging + production since OPS103;
|
# 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) —
|
# STARTS BY ITSELF: a `push` to `main` starts the run (OPS104 trigger 1) —
|
||||||
# the `preflight` job skips it when another deploy run is already
|
# the `preflight` job skips it when another deploy run is already
|
||||||
|
|
@ -28,6 +28,10 @@
|
||||||
# (required reviewer fsolla, configured in the repo).
|
# (required reviewer fsolla, configured in the repo).
|
||||||
# A red staging fail-closes production. Same script,
|
# A red staging fail-closes production. Same script,
|
||||||
# TEQO_ENV=production (teqo-1313, 127.0.0.1:1313).
|
# 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
|
# requeue — hosted, after deploy-staging: re-dispatches when main
|
||||||
# advanced (OPS104 trigger 2).
|
# advanced (OPS104 trigger 2).
|
||||||
#
|
#
|
||||||
|
|
@ -42,11 +46,12 @@
|
||||||
# pre-build on the env DB — OPS66 — and the compose network / local registry
|
# 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
|
# localhost:5000), which is exactly why the hosted jobs only verify/decide
|
||||||
# and the self-hosted jobs only build + deploy. Deploy is idempotent
|
# and the self-hosted jobs only build + deploy. Deploy is idempotent
|
||||||
# ("already deployed" guard in deploy-homeserver.sh) and serialized by flock
|
# ("already deployed" guard in deploy-homeserver.sh) and serialized by ONE
|
||||||
# per environment; the `concurrency` group below serializes the two shared
|
# flock on the host (both environments). The `concurrency` group below lives
|
||||||
# homeserver compose/workspace consumers across runs without cancelling an
|
# ONLY on `deploy-staging` (OPS107) — it never waits on an approval, so it
|
||||||
# in-flight deploy (the staged `needs` chain already serializes them inside
|
# always drains; the staged `needs` chain serializes the environments inside
|
||||||
# one run).
|
# 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
|
# 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
|
# deploy-homeserver.sh defaults there; the script runs to completion for the
|
||||||
|
|
@ -211,11 +216,14 @@ jobs:
|
||||||
runs-on: [self-hosted, homeserver]
|
runs-on: [self-hosted, homeserver]
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
concurrency:
|
concurrency:
|
||||||
# Shared homeserver compose/workspace: never two deploys at once across
|
# Shared homeserver compose/workspace: never two staging deploys at once
|
||||||
# runs. The staged needs chain serializes them within one run. `queue:
|
# across runs. Only staging holds this group (OPS107): it never waits on
|
||||||
# max`: the default single-slot queue cancels a previously pending
|
# an approval, so it always drains — production has NO group because a
|
||||||
# deploy when a new dispatch arrives, even with cancel-in-progress
|
# `waiting` approval would block every later run's staging (the
|
||||||
# false — deploys must wait their turn (OPS102: run to completion).
|
# 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
|
group: deploy-homeserver
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
queue: max
|
queue: max
|
||||||
|
|
@ -250,24 +258,24 @@ jobs:
|
||||||
|
|
||||||
deploy-production:
|
deploy-production:
|
||||||
name: deploy production (teqo-1313)
|
name: deploy production (teqo-1313)
|
||||||
# OPS53 + OPS71 + OPS103 + OPS104. Runs on the GitHub self-hosted runner
|
# OPS53 + OPS71 + OPS103 + OPS104 + OPS107. Runs on the GitHub self-hosted
|
||||||
# ON the homeserver (labels self-hosted + homeserver): the script executes
|
# runner ON the homeserver (labels self-hosted + homeserver): the script
|
||||||
# locally — no SSH hop, no workstation involvement. Never wired in
|
# executes locally — no SSH hop, no workstation involvement. Never wired in
|
||||||
# ci-pr.yml: a self-hosted job is code execution on the production
|
# 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
|
# 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).
|
# (the automatic start and the manual dispatch share the same gate).
|
||||||
# `needs: [deploy-staging]` makes a red staging fail-close production; the
|
# `needs: [deploy-staging]` makes a red staging fail-close production; the
|
||||||
# environment `production` (required reviewer, configured in the repo) is
|
# 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]
|
needs: [deploy-staging]
|
||||||
if: needs.deploy-staging.result == 'success' && github.ref == 'refs/heads/main'
|
if: needs.deploy-staging.result == 'success' && github.ref == 'refs/heads/main'
|
||||||
environment: production
|
environment: production
|
||||||
runs-on: [self-hosted, homeserver]
|
runs-on: [self-hosted, homeserver]
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
concurrency:
|
|
||||||
group: deploy-homeserver
|
|
||||||
cancel-in-progress: false
|
|
||||||
queue: max
|
|
||||||
env:
|
env:
|
||||||
TEQO_ENV: production
|
TEQO_ENV: production
|
||||||
steps:
|
steps:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Deploy & CI
|
## 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`.
|
**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
|
|
@ -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`).
|
> 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`).
|
> 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.
|
> 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`).
|
> 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`.
|
> 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`.
|
> 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`.
|
> 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.
|
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`).
|
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`).
|
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
|
### Running tests
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,13 @@ Agentes **nunca** recebem `DATABASE_URL` de prod/staging e nunca setam `ALLOW_RE
|
||||||
|
|
||||||
```text
|
```text
|
||||||
claim → feature branch → PR --base main (GitHub) → CI PR green (cascade + skips) → auto-merge nativo em main (rebase)
|
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`).
|
**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.
|
- **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.
|
- **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
|
### 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)
|
## CI por alvo (GitHub Actions, OPS71)
|
||||||
|
|
||||||
| Workflow | Trigger | Banco | Passos |
|
| 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) |
|
| `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) |
|
| `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) |
|
| `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 |
|
| `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 |
|
| `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.
|
`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).
|
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
|
## Cursor Cloud
|
||||||
|
|
||||||
|
|
|
||||||
1
docs/changelog/2026-09-14-ops107.md
Normal file
1
docs/changelog/2026-09-14-ops107.md
Normal 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.
|
||||||
|
|
@ -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
|
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
|
`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-staging` → aprovação do environment `production` →
|
||||||
`deploy-production` (OPS103).
|
`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
|
## Gatilho e fluxo
|
||||||
|
|
||||||
1. **Início automático (OPS104):** merge em `main` → o run nasce no head; o
|
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
|
job `preflight` (hosted) pula o run quando já existe outro deploy
|
||||||
`queued`/`in_progress`. Um run aguardando aprovação de produção (`waiting`)
|
`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
|
**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
|
Actions → **Deploy** → Run workflow, ref: `main`) segue como escape manual e
|
||||||
**não** passa pelo guard.
|
**não** passa pelo guard.
|
||||||
2. Job `preflight` (hosted, `scripts/deploy-preflight.mjs`): consulta os runs
|
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
|
`environment: production`) **aguarda a aprovação do reviewer** e então
|
||||||
executa `TEQO_ENV=production bash scripts/deploy-homeserver.sh <sha>` —
|
executa `TEQO_ENV=production bash scripts/deploy-homeserver.sh <sha>` —
|
||||||
publica o **mesmo SHA** em produção (`teqo-1313`, `127.0.0.1:1313`).
|
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 é
|
Staging vermelho = produção nunca roda. O job de produção **não tem**
|
||||||
serializada pela `concurrency: deploy-homeserver`.
|
`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`
|
6. **Requeue (OPS104):** com o staging verde, o job hosted `requeue`
|
||||||
(`scripts/deploy-requeue.mjs`, **fora** do grupo `deploy-homeserver` —
|
(`scripts/deploy-requeue.mjs`, **fora** do grupo `deploy-homeserver` —
|
||||||
um run aguardando aprovação não pode bloquear o requeue) compara o head de
|
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
|
um run novo no head novo. Falha aqui é fail-red (job vermelho): staging
|
||||||
velho nunca fica silencioso.
|
velho nunca fica silencioso.
|
||||||
7. O script, no **homeserver**: `flock` único (compose, workspace e registry
|
7. O script, no **homeserver**: `flock` único (compose, workspace e registry
|
||||||
são compartilhados — um deploy por vez no host; o `concurrency` do
|
são compartilhados — um deploy por vez no host; desde a OPS107 o
|
||||||
workflow serializa os runs e o lock cobre invocação manual; desde a OPS102
|
`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
|
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
|
verify; o workspace é clonado/atualizado de `TEQO_REPO_URL` — default
|
||||||
`https://github.com/fsolla/teqo.git`, público) →
|
`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 é
|
**Primeiro deploy (verificação ao vivo):** o caminho de estreia do OPS104 é
|
||||||
mergear em `main` e observar o run nascer sozinho: log do `preflight`
|
mergear em `main` e observar o run nascer sozinho: log do `preflight`
|
||||||
(`should_deploy=true`), `verify` verde, staging publicado e `deploy-production`
|
(`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
|
`deploy-production` o `set-url` do workspace (origem Forgejo → GitHub), o
|
||||||
`deployed_sha` lido do container e o guard "already deployed" num segundo run
|
`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,
|
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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| Deploy dispara mas o job `verify` falha | Regressão real na suíte full | Corrigir e re-dispatchar — o homeserver nunca é tocado |
|
||||||
|
|
|
||||||
132
docs/plans/ops107-aprovacao-producao-escolhida-impl.md
Normal file
132
docs/plans/ops107-aprovacao-producao-escolhida-impl.md
Normal 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 |
|
||||||
|
|
@ -49,8 +49,10 @@ WORKSPACE_DIR="${WORKSPACE_DIR:-$HOME/teqo-deploy}"
|
||||||
TEQO_REGISTRY="${TEQO_REGISTRY:-localhost:5000}"
|
TEQO_REGISTRY="${TEQO_REGISTRY:-localhost:5000}"
|
||||||
# ONE lock for both environments: the compose file, the workspace and the
|
# ONE lock for both environments: the compose file, the workspace and the
|
||||||
# local registry are shared, so staging and production must never build or
|
# local registry are shared, so staging and production must never build or
|
||||||
# swap concurrently (the workflow's job-level concurrency serializes the
|
# swap concurrently (the workflow's job-level concurrency lives only on
|
||||||
# runs; this lock also covers manual invocations on the host).
|
# 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}"
|
DEPLOY_LOCK="${DEPLOY_LOCK:-/tmp/teqo-deploy.lock}"
|
||||||
|
|
||||||
say() { printf '[deploy] %s\n' "$*"; }
|
say() { printf '[deploy] %s\n' "$*"; }
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ describe('ciSkipInvariants', () => {
|
||||||
expect(ciPr).not.toContain('ci-classify-production.mjs')
|
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
|
// One dispatch, one verify, two separately approved deploys. The gate is
|
||||||
// the GitHub Environment (reviewer on production, configured in the repo —
|
// 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
|
// 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(' TEQO_ENV: production')
|
||||||
expect(deploy).toContain("github.ref == 'refs/heads/main'")
|
expect(deploy).toContain("github.ref == 'refs/heads/main'")
|
||||||
expect(deploy).toContain("needs.deploy-staging.result == 'success'")
|
expect(deploy).toContain("needs.deploy-staging.result == 'success'")
|
||||||
// Deploys share the homeserver compose/workspace: serialize them across
|
// OPS107: only staging holds the shared lane. Staging never waits on an
|
||||||
// runs without cancelling an in-flight deploy, and queue (never cancel) a
|
// approval, so it always drains; the production approval must NOT hold a
|
||||||
// previously pending one (`queue: max`).
|
// concurrency group — a `waiting` job blocks every later run's staging
|
||||||
expect(deploy).toContain(' group: deploy-homeserver')
|
// (the bug this delivers). Real serialization: the needs chain inside a
|
||||||
expect(deploy).toContain(' cancel-in-progress: false')
|
// run, the single self-hosted runner and the host `flock`.
|
||||||
expect(deploy).toContain(' queue: max')
|
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', () => {
|
it('every campaign domain dir is covered by the e2e affected manifest', () => {
|
||||||
|
|
|
||||||
|
|
@ -207,8 +207,9 @@ describe('scripts/deploy-homeserver.sh environment parameterization (OPS103)', (
|
||||||
|
|
||||||
it('serializes both environments on ONE shared lock (compose/workspace are shared)', () => {
|
it('serializes both environments on ONE shared lock (compose/workspace are shared)', () => {
|
||||||
// Per-env locks would let staging and production build/swap concurrently
|
// Per-env locks would let staging and production build/swap concurrently
|
||||||
// on the same workspace and compose file (the workflow concurrency does
|
// on the same workspace and compose file (the workflow job-level
|
||||||
// not cover manual invocations on the host).
|
// 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).toContain('DEPLOY_LOCK="${DEPLOY_LOCK:-/tmp/teqo-deploy.lock}"')
|
||||||
expect(script).not.toContain('/tmp/teqo-1313-deploy.lock')
|
expect(script).not.toContain('/tmp/teqo-1313-deploy.lock')
|
||||||
expect(script).not.toContain('/tmp/teqo-staging-deploy.lock')
|
expect(script).not.toContain('/tmp/teqo-staging-deploy.lock')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue