mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
Merge pull request #251 from melgarafael/fix/canal-stable-divergente
fix(ci): dois gatilhos para a mesma tag — `stable` e `1.3.0` são builds diferentes
This commit is contained in:
@@ -15,8 +15,27 @@ on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
branches: ["main"]
|
||||
release:
|
||||
types: [published]
|
||||
# NÃO acrescente `release: types: [published]` aqui. Ele já esteve, e o efeito
|
||||
# foi medido na v1.3.0: `gh release create` empurra a tag E publica a release,
|
||||
# então o MESMO commit foi construído DUAS vezes, com 5 minutos de diferença.
|
||||
#
|
||||
# 19:53:04 evento=push v1.3.0 -> publicou 1.3.0, 1.3 e stable
|
||||
# 19:58:05 evento=release v1.3.0 -> reconstruiu e MOVEU 1.3.0 e 1.3,
|
||||
# sem mover stable (a condição exige push)
|
||||
#
|
||||
# Duas consequências, e a segunda é a grave:
|
||||
#
|
||||
# 1. `stable` e `1.3.0` ficaram em digests DIFERENTES (mesmo `revision`
|
||||
# 9bd59e9 e mesmo `version`, builds distintos) — quem comparasse os dois
|
||||
# concluiria, com razão, que são artefatos diferentes.
|
||||
# 2. A tag de VERSÃO foi movida depois de publicada. A doutrina inteira se
|
||||
# apoia em "instalação de cliente aponta para número de versão, nunca para
|
||||
# tag móvel" (docs/doctrine/packaging.md, invariante 3) — e era o nosso
|
||||
# próprio workflow movendo o número. Republicar uma release antiga (editar
|
||||
# e salvar) reconstruiria e moveria aquela versão de novo.
|
||||
#
|
||||
# O gatilho de push da tag já cobre o fluxo real: a tag é sempre empurrada,
|
||||
# com ou sem release. Vigiado por tests/unit/packaging-artefato-do-cliente.test.ts.
|
||||
# Em PR a imagem é CONSTRUÍDA e não publicada. Sem isto, nada no PR consegue
|
||||
# revelar que a mudança quebra a imagem — foi assim que um bump de `next`
|
||||
# passou por `verify`, `build-and-size`, `invariants` e `e2e` (os quatro
|
||||
|
||||
@@ -71,7 +71,7 @@ Resend + Nuvemshop; é a P0 da doutrina de QA) — ou seja, `e2e` verde não pro
|
||||
instalação fresca. `followup-journey`, `webhooks` e `capacidades-do-agente` estiveram fora e
|
||||
**voltaram**: rodam hoje (`e2e.yml`, listas `SPECS_PARTE_1`/`SPECS_PARTE_2`).
|
||||
|
||||
`.github/workflows/imagens.yml`: `imagens-ok` = as três imagens Docker constroem. **Obrigatório
|
||||
`.github/workflows/publish-image.yml`: `imagens-ok` = as três imagens Docker constroem. **Obrigatório
|
||||
desde 2026-08-13.**
|
||||
|
||||
**Os cinco são checks obrigatórios** na branch protection da `main` — medido em 2026-08-14 @ `741c4ec8`:
|
||||
|
||||
@@ -262,7 +262,7 @@ Checks **obrigatórios** na branch protection da `main` (verificado na configura
|
||||
- **`invariants`** (`ci.yml`) — `pnpm test:db`: sobe `pgvector/pgvector:pg17`, aplica `supabase/baseline.sql` em modo install (`ON_ERROR_STOP=1`) e update (idempotência), e roda os testes de invariante, incluindo o de isolamento RLS entre 2 organizações.
|
||||
- **`build-and-size`** (`perf.yml`) — `pnpm build` em Node 22.
|
||||
- **`e2e`** (`e2e.yml`) — sobe Supabase local, aplica o `baseline.sql` e roda **45 das 46 specs** Playwright (medido em 2026-08-14 @ `741c4ec8`; **reconte antes de citar**, com `ls tests/e2e/*.spec.ts | wc -l` e `grep -oE '[a-z0-9-]+\.spec\.ts' .github/workflows/e2e.yml | sort -u | wc -l` — este número já apodreceu **três** vezes, e uma delas foi eu copiando o `echo` do próprio workflow em vez de contar os arquivos). A **única** de fora é `vps-fresh-onboarding` (precisa de WAHA + Redis + Resend + Nuvemshop) — e ela é a **P0** da doutrina de QA Visual, ou seja, `e2e` verde **não** prova a jornada de instalação fresca, que é o produto que se vende.
|
||||
- **`imagens-ok`** (`imagens.yml`) — reprova quando qualquer uma das três imagens Docker não constrói. **É obrigatório desde 2026-08-13**; este arquivo dizia o contrário em outro parágrafo (ver a doutrina de packaging acima, já corrigida).
|
||||
- **`imagens-ok`** (`publish-image.yml`) — reprova quando qualquer uma das três imagens Docker não constrói. **É obrigatório desde 2026-08-13**; este arquivo dizia o contrário em outro parágrafo (ver a doutrina de packaging acima, já corrigida).
|
||||
|
||||
Todos os **cinco** são **obrigatórios** — medido em 2026-08-14 na branch protection:
|
||||
|
||||
|
||||
+3
-3
@@ -77,10 +77,10 @@ Ao finalizar um epic:
|
||||
- `pnpm test:e2e` (subset relevante) — **opcional se você contribui de fora**, ver abaixo
|
||||
4. Abrir PR contra `main`. Description deve referenciar o epic e listar evidências (logs/screenshots dos testes).
|
||||
5. CI deve passar antes de merge. Obrigatórios: `verify`, `invariants` (isolamento RLS),
|
||||
`build-and-size` e `e2e`.
|
||||
`build-and-size`, `e2e` e `imagens-ok`.
|
||||
|
||||
O job `imagens-ok` (constrói as três imagens que o self-hoster instala) roda em PR e
|
||||
**ainda não bloqueia** — a ativação depende de um passo de administração do repositório.
|
||||
O `imagens-ok` (em `.github/workflows/publish-image.yml`) constrói as três imagens que o
|
||||
self-hoster instala, roda em PR e **bloqueia** desde 2026-08-13.
|
||||
|
||||
Verde no `e2e` **não** é "jornada provada": ele mesmo imprime, no resumo, quais specs não
|
||||
cobriu — e a que fica de fora é justamente `vps-fresh-onboarding`, a instalação do zero.
|
||||
|
||||
@@ -42,8 +42,11 @@ O procedimento completo está em [`doctrine/packaging.md`](doctrine/packaging.md
|
||||
- [ ] `git tag vX.Y.Z && git push origin vX.Y.Z` a partir de um commit da `main`
|
||||
- [ ] `gh run list --workflow=publish-image.yml --limit 3` → verde
|
||||
- [ ] As **três** imagens existem na versão: `deskcommcrm`, `deskcomm-worker`, `deskcomm-scheduler`
|
||||
- [ ] `stable` aponta para esta versão (mesmo digest de `X.Y.Z`)
|
||||
- [ ] `gh release create vX.Y.Z` com as notas do CHANGELOG
|
||||
- [ ] **Depois da release**, `stable` e `X.Y.Z` são o mesmo digest nas três imagens.
|
||||
Nesta ordem, e não antes: na v1.3.0 a conferência rodou antes do
|
||||
`gh release create`, passou, e o próprio `release` republicou a versão em
|
||||
cima — verde às 19:53, divergente às 19:58.
|
||||
|
||||
**Provar (o item que exige VPS, e não é opcional)**
|
||||
|
||||
|
||||
+39
-15
@@ -104,22 +104,24 @@ OCI — no mínimo `source`, `revision`, `version`, `licenses` — e é constru
|
||||
das três imagens não constrói. Ele existe porque a matriz gera um nome de check por imagem,
|
||||
e exigir os três pelo nome faria uma quarta imagem, um dia, escapar do gate em silêncio.
|
||||
|
||||
> **Pendência de ativação — leia antes de citar esta linha como garantia.** `imagens-ok`
|
||||
> **ainda não está** na branch protection. Medido em 2026-08-13:
|
||||
> **Ativado.** `imagens-ok` **é** required check da `main`. Medido em 2026-08-14:
|
||||
>
|
||||
> ```console
|
||||
> $ gh api repos/melgarafael/DeskcommCRM/branches/main/protection \
|
||||
> --jq '.required_status_checks.contexts|join(", ")'
|
||||
> verify, build-and-size, invariants, e2e
|
||||
> verify, build-and-size, invariants, e2e, imagens-ok
|
||||
> ```
|
||||
>
|
||||
> Enquanto isso valer, este invariante é **conselho, não gate**. A ativação é o último passo
|
||||
> do merge desta doutrina, e não pode vir antes: um required check que não existe na base
|
||||
> dos PRs já abertos bloqueia todos eles até que cada um rebase. O roteiro, com as
|
||||
> verificações de cada passo, está em
|
||||
> [`../runbooks/ativar-packaging.md`](../runbooks/ativar-packaging.md). Uma versão anterior deste
|
||||
> parágrafo afirmava, no presente, que o check já era obrigatório — exatamente o defeito que
|
||||
> esta doutrina existe para impedir, cometido dentro dela.
|
||||
> Este parágrafo já disse as duas coisas erradas, em ordem: primeiro afirmou no presente
|
||||
> que o check era obrigatório quando não era, depois — corrigido — afirmou que "ainda não
|
||||
> está" e **continuou afirmando isso depois da ativação**, que aconteceu no mesmo dia. O
|
||||
> segundo erro é o mais instrutivo: o texto foi escrito *sabendo* que a ativação era o
|
||||
> passo seguinte, e ninguém volta para trocar um "ainda não" por um "já". **Nota de
|
||||
> pendência é dívida com data de vencimento e sem cobrador.** Quem ler qualquer uma das
|
||||
> duas versões mede contra a régua errada — reconfira na fonte, com o comando acima.
|
||||
>
|
||||
> O roteiro da ativação, com as verificações de cada passo, está em
|
||||
> [`../runbooks/ativar-packaging.md`](../runbooks/ativar-packaging.md).
|
||||
|
||||
O gate importa porque já falhou: em 2026-08-12 um bump de `next` passou pelos quatro
|
||||
obrigatórios e quebrou o build da imagem na `main`, porque o `next build` dentro do
|
||||
@@ -332,24 +334,46 @@ do banco. É o passo que mais trava na estreia de uma imagem nova.
|
||||
for i in deskcommcrm deskcomm-worker deskcomm-scheduler; do
|
||||
echo "$i: $(ghcr_status $i X.Y.Z)"; done → 200 nas três
|
||||
403 em alguma? Torne o pacote público ANTES de seguir
|
||||
[ ] 8. `stable` aponta para esta versão (mesmo digest de X.Y.Z):
|
||||
ghcr_status deskcommcrm stable → 200, e o digest bate
|
||||
[ ] 9. A imagem reporta a versão certa:
|
||||
[ ] 8. A imagem reporta a versão certa:
|
||||
docker run --rm ghcr.io/melgarafael/deskcommcrm:X.Y.Z \
|
||||
node -e 'console.log(process.env.APP_VERSION)' → X.Y.Z
|
||||
[ ] 10. `gh release create vX.Y.Z` com as notas do CHANGELOG
|
||||
[ ] 9. `gh release create vX.Y.Z` com as notas do CHANGELOG
|
||||
[ ] 10. SÓ AGORA: `stable` e X.Y.Z são o MESMO digest, nas três imagens:
|
||||
for i in deskcommcrm deskcomm-worker deskcomm-scheduler; do
|
||||
for t in X.Y.Z stable; do
|
||||
echo -n "$i:$t "; docker buildx imagetools inspect \
|
||||
ghcr.io/melgarafael/$i:$t --format '{{.Manifest.Digest}}'; done; done
|
||||
→ o par de cada imagem tem que bater
|
||||
Não bateu? Alguma coisa republicou depois do push da tag. NÃO siga:
|
||||
um canal apontando para build diferente da versão é o invariante 3
|
||||
quebrado dentro de casa.
|
||||
[ ] 11. Apagar tags de branch dos três pacotes — `docs-doutrina-packaging` e
|
||||
qualquer outra que tenha nascido de um `workflow_dispatch` de ensaio.
|
||||
Tag de branch é artefato de trabalho: se ficar, vira canal órfão que
|
||||
alguém pina por engano achando que é release, e ela nunca mais se move.
|
||||
O registry já carrega uma dessas (`quebrada-teste`) como lembrete.
|
||||
|
||||
> **Por que a checagem de `stable` é o item 10 e não o 8.** Ela já foi o 8, antes do
|
||||
> `gh release create` — e nessa ordem ela não provava nada. Medido na v1.3.0: o
|
||||
> `release: published` estava ligado no workflow, `gh release create` disparou um
|
||||
> segundo build do mesmo commit, e esse build **moveu `1.3.0` e `1.3`** sem mover
|
||||
> `stable`. A conferência do item 8 tinha passado, verde e honesta, cinco minutos
|
||||
> antes do ato que a invalidou. **Verificação que roda antes do passo que pode
|
||||
> quebrá-la é verificação de nada.** O gatilho foi removido (guarda em
|
||||
> `tests/unit/packaging-artefato-do-cliente.test.ts`), e a conferência foi para
|
||||
> depois — cinto e suspensório, porque o próximo jeito de republicar uma tag ainda
|
||||
> não foi inventado.
|
||||
|
||||
EXIGE ESCOPO QUE O TOKEN PADRÃO DO `gh` NÃO TEM. Medido no corte da
|
||||
1.3.0: com `gist, read:org, repo, workflow` a API devolve 403 tanto para
|
||||
listar quanto para apagar versão de pacote. Antes de chegar aqui:
|
||||
gh auth refresh -h github.com -s read:packages,delete:packages
|
||||
Sem isso o item fica pendente e a tag de ensaio segue viva — foi o que
|
||||
aconteceu na 1.3.0.
|
||||
aconteceu na 1.3.0. (Resolvido em 2026-08-14: as três versions foram
|
||||
apagadas e a tag responde 404 nos três pacotes. Apague a **version**, e
|
||||
só depois de conferir que ela não carrega OUTRA tag junto — no GHCR se
|
||||
apaga version, não tag, e uma version com `1.3.0` ao lado levaria a
|
||||
release embora.)
|
||||
[ ] 12. Ensaio de atualização numa instalação real (não fresca): update.sh a partir da
|
||||
versão anterior, e o /api/v1/health responde X.Y.Z
|
||||
```
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# Runbook — ativar a doutrina de packaging (uma vez só)
|
||||
|
||||
> **CONCLUÍDO em 2026-08-14. Este runbook é histórico** — guardado porque descreve o
|
||||
> procedimento e as armadilhas de cada passo, não porque haja algo a fazer.
|
||||
>
|
||||
> | Passo | Estado | Prova |
|
||||
> |---|---|---|
|
||||
> | 1–2. Pacotes públicos | feito | `docker pull` anônimo resolve as três |
|
||||
> | 3. `imagens-ok` obrigatório | feito | `protection` → `verify, build-and-size, invariants, e2e, imagens-ok` |
|
||||
> | 4. Primeira release completa | feito | v1.3.0; `1.3.0` e `stable` nos três pacotes |
|
||||
> | 5. Tag de ensaio apagada | feito | `docs-doutrina-packaging` → 404 nos três |
|
||||
>
|
||||
> Reconfira na fonte antes de confiar nesta tabela — foi por confiar numa nota de estado
|
||||
> que a doutrina passou um dia inteiro afirmando que o `imagens-ok` não bloqueava, depois
|
||||
> de ele já bloquear.
|
||||
|
||||
Este documento existe porque a entrega da [doutrina de packaging](../doctrine/packaging.md)
|
||||
tem três passos que **não podem estar dentro do PR**: dois dependem de administração do
|
||||
repositório e um depende de as imagens existirem. Enquanto eles não forem dados, parte da
|
||||
|
||||
@@ -231,6 +231,38 @@ describe("packaging — o artefato que o cliente instala", () => {
|
||||
);
|
||||
expect(wf, "publish-image.yml não cria o canal 'stable'").toContain("value=stable");
|
||||
});
|
||||
|
||||
it("nenhum gatilho reconstrói uma tag já publicada", () => {
|
||||
// Medido na v1.3.0: com `release: types: [published]` ligado ao lado de
|
||||
// `push: tags`, `gh release create` disparou DOIS runs para o mesmo commit
|
||||
// (19:53 push, 19:58 release). O segundo moveu `1.3.0` e `1.3` para um build
|
||||
// novo e deixou `stable` no antigo — digests divergentes para o mesmo
|
||||
// `revision`, e uma tag de VERSÃO movida depois de publicada, que é
|
||||
// exatamente o que o invariante 3 da doutrina proíbe.
|
||||
//
|
||||
// Este teste guarda o COMPORTAMENTO (o que dispara um build que publica),
|
||||
// não a ausência de uma string: qualquer gatilho novo que re-publique sobre
|
||||
// uma tag existente reprova aqui, não só o `release`.
|
||||
const wf = fs.readFileSync(path.join(RAIZ, ".github/workflows/publish-image.yml"), "utf8");
|
||||
const gatilhos = wf.split(/^jobs:/m)[0] ?? "";
|
||||
const semComentarios = gatilhos
|
||||
.split("\n")
|
||||
.filter((l) => !/^\s*#/.test(l))
|
||||
.join("\n");
|
||||
|
||||
for (const proibido of ["release:", "workflow_run:", "schedule:", "repository_dispatch:"]) {
|
||||
expect(
|
||||
semComentarios,
|
||||
`publish-image.yml reagiria a '${proibido}' — esse gatilho pode reconstruir e MOVER uma tag de versão já publicada`,
|
||||
).not.toContain(proibido);
|
||||
}
|
||||
|
||||
// E o gatilho que precisa existir continua existindo — sem esta linha o
|
||||
// teste passaria num workflow que não publica coisa nenhuma.
|
||||
expect(semComentarios, "publish-image.yml deixou de reagir a push de tag").toMatch(
|
||||
/tags:\s*\["?v\*/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("packaging — a versão que roda é observável de fora", () => {
|
||||
|
||||
Reference in New Issue
Block a user