Runbook de Release
Como cortar uma release vX.Y.Z do GarraIA. Tudo depois do tag é automático.
1. Preparar (via PR — o main é protegido)
-
Bump da versão do workspace em
Cargo.toml([workspace.package] version). Junto com o bump:catalog_versionemcrates/garraia-gateway/assets/catalogo-funcionalidades.jsonrecebe a mesma versão — o testeversao_do_catalogo_casa_com_a_versao_do_workspace(#1670) falha se as duas divergirem. Antes de subir o número, confira se o catálogo descreve o que a versão entrega (itens novos, pré-requisitos e limitações que mudaram): é dele que o Garra tira a resposta sobre as próprias funcionalidades. -
Rodar
cargo checkpara oCargo.lockacompanhar (nunca editar o lock à mão). -
Juntar os fragmentos de changelog acumulados desde a última versão:
python3 scripts/changelog/assemble.py # confere o que vai entrarpython3 scripts/changelog/assemble.py --write # insere no [Unreleased] e apaga os fragmentosOs PRs não editam mais o
CHANGELOG.mddireto — cada um deixa um arquivo emchangelog.d/<seção>/(verchangelog.d/README.md), justamente para dois PRs paralelos não colidirem na mesma seção. Este passo é onde os fragmentos viram changelog de verdade. Revise o texto agregado antes de seguir: o script junta, não escreve por você. -
CHANGELOG.md: mover o conteúdo de## [Unreleased]para uma nova seção## [X.Y.Z] - AAAA-MM-DD(data narrativa em horário da Flórida, ver CLAUDE.md §Convenção de datas).[Unreleased]volta vazio. -
Abrir PR, aguardar CI verde e mergear. A ruleset "Protect main" exige 7 checks obrigatorios (confira na API:
GET /repos/michelbr84/GarraRUST/rulesets→ o ruleset ativo): Format Check, Clippy Linting, Test (ubuntu-latest), Test (windows-latest), Security Gate (BOLA & Tenant Isolation), Auth Integration (test-support) e Gateway Integration (Postgres). Preserve os sete — o RC nao afrouxa gate obrigatorio.
1.5 Gate de dogfood — obrigatório antes do tag (#1439)
O CI prova que o código compila e que os testes passam; não prova que uma
instalação limpa funciona para uma pessoa. A v0.4.4 e a v0.4.5 saíram verdes
e quebraram no caminho real (WhatsApp numa instalação nova, NoRoots,
status que mentia). Por isso, antes de empurrar o tag, alguém com máquina e
telefone executa a matriz abaixo contra o candidato e registra data, sistema
e quem executou. Linha sem data = release não sai.
Onde o candidato existe — e onde ele NÃO existe. O release.yml não tem
modo de teste: seu workflow_dispatch aceita um version, mas o job release
usa softprops/action-gh-release com draft: false — ele cria o tag no commit
do run e publica a Release de verdade. Dispará-lo esperando artefato de teste
publica a release sem querer. (Esta seção afirmava o contrário até 2026-09-28.)
Fora dele, cada grupo de linhas tem uma origem de candidato diferente, e três delas não precisam de tag nenhum:
| Linhas | Candidato, sem publicar nada |
|---|---|
| D1, D3a, D9-linux | scripts/dogfood/linux-clean-install.sh --source local no checkout do candidato — empacota com o mesmo packaging/nfpm.yaml da release e instala num ubuntu:24.04 cru |
| D5, D6 | gh workflow run desktop.yml --ref <sha> — o desktop.yml tem workflow_dispatch, permissions: contents: read e nenhum passo de publicação: ele constrói MSI + NSIS no windows-latest e .deb + AppImage no ubuntu-22.04 e sobe os bundles como artifacts (garraia-desktop-windows-pr, garraia-desktop-linux-pr). Baixar o artifact do run e instalar à mão é o candidato de desktop |
| D2 | precisa de asset publicado, mas aceita um tag pinado: install.ps1 -Version <tag> / $env:GARRAIA_VERSION (e install.sh --version <tag>) baixam a tag pedida em vez da latest. Um prerelease vX.Y.Z-rcN serve |
| D4 | não tem candidato pré-tag, por desenho do código. crates/garraia-cli/src/update.rs:9 fixa RELEASES_API em .../releases/latest — sem flag, sem env var, sem override. Testar garra update da versão anterior para a nova exige que a nova seja a latest, isto é, exige a publicação |
Daí duas consequências que valem como regra, não como opinião:
- D4 e D8 são pós-publicação por desenho, não pré-requisitos do tag. No D8 as
URLs do
garraia.orgresolvem a releaselatest; no D4 é o próprioupdate.rsque só conhece alatest. Exigir os dois antes do tag é uma dependência circular — eles são verificação do §4.8, e uma falha ali vira a versão seguinte. - D1b, D3b, D7b, D10 e o trecho WhatsApp do D1 continuam manuais por desenho: precisam de dois telefones reais e de leitura humana da resposta do modelo. Nenhum caminho de build os automatiza.
Para o que sobra, os caminhos reais, e a escolha é do dono:
- Prerelease
vX.Y.Z-rcNpor push de tag — produz assets reais e e marcado prerelease automaticamente (release.ymlusaprerelease: truee, desde o preparo do RC,make_latest: falseexplicito), entao nao viralatestnem e servido por/releases/latest, e nem oinstall.shnem ogarra updateo alcancam. Habilita D2 (via-Version vX.Y.Z-rcN). Ponto de atencao sobre o MSI: oProductVersiondo WiX e numerico de tres partes e nao expressa prerelease semver, mas quem o alimenta e a versao do Cargo (ocrates/garraia-desktop/src-tauri/tauri.conf.jsonnao declaraversion, entao o bundler Tauri leversion.workspacedoCargo.toml), nao a tag git. Com o bump de RC mantido puro (0.4.10noCargo.toml, sem-rcN— ver a secao "Pre-lancamento (RC)"), o job do MSI e os demais bundlers desktop passam no tag-rc, e o step fail-closed dolatest.json(que recusa sem ao menos um.sigde updater) tambem. Fosse a versao do Cargo0.4.10-rc1, o WiX quebraria. - Aceite de risco registrado, como na v0.4.6 (PR #1522): tagear com a cobertura que existe e rodar as linhas manuais depois, com o compromisso escrito de que uma falha vira a versão seguinte. A decisão vai no corpo da PR de release e no corpo da Release.
| # | Cenário | Onde | O que prova | Automatizado? |
|---|---|---|---|---|
| D1 | Instalação limpa da CLI (curl | sh), garraia init com um provedor, garraia whatsapp link, um número autorizado, "oi" → resposta real | Ubuntu 22.04 limpo, com Node 20+ | o caminho que o usuário faz | parcial — Linux automatizado até "resposta real" por scripts/dogfood/linux-clean-install.sh (.deb num ubuntu:24.04 cru, provedor Ollama local, sessão REST, restart, stop); o whatsapp link e o número autorizado continuam manuais |
| D2 | Mesmo cenário via irm | iex | Windows 10/11 | paridade dos instaladores (regra 16) | não |
| D3 | garraia restart → nova mensagem responde sem QR; allow sobrevive | Ubuntu + Windows | persistência da sessão e da política | fixture serve-echo + manual |
| D4 | garraia update da release anterior para o candidato; garraia rollback | Ubuntu | o contrato dos assets crus (regra 15) e o .old | não — e pós-publicação por desenho: update.rs:9 só conhece releases/latest |
| D5 | Desktop: MSI instala, papagaio e Chat Bar aparecem, garraia status no terminal mostra o sidecar, sair encerra o sidecar | Windows 11 | o bundle e o sidecar | manual, mas o bundle sai sem tag pelo desktop.yml (dispatch → artifact garraia-desktop-windows-pr) |
| D6 | Desktop: .deb idem | Ubuntu 22.04 (X11) | idem | manual, mas o bundle sai sem tag pelo desktop.yml (artifact garraia-desktop-linux-pr) |
| D7 | Duas identidades autorizadas pedem list_dir; nenhuma vê os arquivos da outra | Ubuntu | isolamento do workspace por sessão (#1449) | teste de integração + manual |
| D8 | install-endpoints.yml verde depois de publicar | — | garraia.org serve os instaladores (regra 17) | sim (workflow) |
| D9 | GET /api/diagnostics numa instalação limpa sem nenhum aviso espúrio; garraia doctor exit 0 | Ubuntu + Windows | honestidade do status (#1437, #1387) | parcial — Linux automatizado pelo mesmo script (doctor --json exit 0, doctor whatsapp --json exit 69 com a linha whatsapp.linked, /api/health healthy); Windows manual |
| D10 | Perguntas diretas de capacidade ao agente ("Você tem MCP?", "Pode escrever arquivos?", "Por que não lê esta pasta?") numa sessão standard e numa isolated-pod | Ubuntu + Windows | a resposta do modelo bate com o registro de capacidades — nada de "não tenho MCP" com MCP escondido nem "não tenho filesystem" com workspace só esperando seleção (#1428, #1387) | não — os fundamentos (nota do prompt e registro de capacidades) são cobertos em CI por nota_garra_status.rs e pelos testes do registro; a resposta do modelo continua manual |
D1/D9 Linux — como rodar a automação (#1426). Numa máquina com
Docker e Ollama (ollama pull qwen3.5:0.8b), a partir do checkout do
candidato:
scripts/dogfood/linux-clean-install.sh --source local # empacota este checkout com o nfpm da release
scripts/dogfood/linux-clean-install.sh --run-id <run-id> # ou o .deb de um run já verde do release.yml (testa a release ANTERIOR)
O script sobe um ubuntu:24.04 sem nada de desenvolvimento, instala o
.deb com apt-get install, roda os doctors, aponta o provedor para o
Ollama do host (sem chave paga), sobe o gateway na 3899 (nunca na 3888
do host), exige uma resposta real do modelo por REST, reinicia e prova que
a sessão e a config sobreviveram, e encerra com garraia stop. A evidência
inteira (logs, JSONs, resposta do modelo, .deb testado) fica em
dogfood/linux/<data-hora>/ (gitignored) com um resumo.txt PASSOU/FALHOU
por passo — é esse resumo que vai na tabela da PR de release. Sem
--source/--run-id, o default baixa o pacote do último run verde do
release.yml, o que testa a release anterior e não o candidato: serve
para reproduzir um bug de campo, não para o gate.
Regras do gate:
- Contra o candidato, não contra
main: o que se testa é o que vai ser publicado. - Segredos fora da evidência: transcrições redigidas, telefones só pelos quatro últimos dígitos, nenhuma chave em screenshot.
- Falhou, não sai: uma linha vermelha vira issue com o log e a release espera a correção — nunca "sai e conserta na próxima".
- A tabela preenchida vai no corpo da PR de release (bump + CHANGELOG), para ficar ao lado do que ela libera.
A automação do que dá para automatizar (D1 com ponte falsa, D3, D7, D9) é a
issue #1426; a parte Linux de D1/D9 já roda pelo script acima, o resto (D2,
D4, D5, D6 e o trecho WhatsApp de D1) segue manual e está aqui de propósito
— um gate que só existe numa issue não gateia nada. O desenho completo, com
o que o CI cobre e o que só pessoa cobre, está em
plans/0364-release-v0.4.6-preflight.md §6.
1.6 Pre-lancamento (RC)
Quando o dono autoriza publicar um candidato (vX.Y.Z-rcN) como
pre-release, antes da estavel. O objetivo e provar o pacote no mundo real
sem mover o canal estavel: a automacao marca o RC como prerelease, nao o
torna latest e nao o distribui pelo canal de atualizacao estavel.
Runbook (exemplo v0.4.10-rc1):
-
Branch
release/v0.4.10. Ocheck_presence.pyisenta exatamente^release/v\d+\.\d+\.\d+$deste repo (scripts/changelog/check_presence.py:56), entao a PR de bump+nota nao precisa de fragmento proprio. Qualquer outro nome de branch continua cobrado. -
Bump PURO:
0.4.10emCargo.toml([workspace.package] version), sem-rcN. O-rcNvive so na tag git. Motivo: oProductVersiondo WiX e o campoversiondolatest.jsonvem da versao do Cargo (otauri.conf.jsonnao declaraversion); uma versao de Cargo com-rcquebraria o job do MSI e poderia derrubar o step fail-closed dolatest.json(§1.5, item 1).cargo checkpara oCargo.lockacompanhar, e ocatalog_versiondo catalogo de funcionalidades sobe para o mesmo0.4.10(§1 passo 1). -
NAO rode
assemble.py --write. Juntar os fragmentos no[Unreleased]e apaga-los e passo da estavel (§1 passo 3). No RC os fragmentos ficam emchangelog.d/— eles entram de uma vez quando a estavel sair. -
CHANGELOG (opcional): se quiser uma nota no corpo da Release, adicione uma secao curta
## [0.4.10-rc1] - AAAA-MM-DD(data narrativa, horario da Florida) com um paragrafo de abertura — oscripts/release/notes.pyextrai so a prosa antes da primeira subsecao###. Nao mova os fragmentos para dentro dela. -
Merge da PR com os 7 checks verdes (§1 passo 5).
-
Dogfood no CI (§1.5): Actions → Dogfood Linux (clean install) → Run workflow,
ref = release/v0.4.10(antes do tag) ouref = v0.4.10-rc1(depois). Rodascripts/dogfood/linux-clean-install.sh --source localnumubuntu-latest, falha se oresumo.txtdisser FALHOU e sobedogfood/linux/**como artifact. (Oworkflow_dispatchso aparece depois quedogfood-linux.ymlesta na default branch.) -
Tag por push (nunca
workflow_dispatch— ele publicaria a Release mesmo, §1.5):git fetch origin maingit tag v0.4.10-rc1 <sha-do-merge>git push origin v0.4.10-rc1 -
Conferencias pos-run (a automacao, nao o humano, garante o canal):
GET /repos/michelbr84/GarraRUST/releases/tags/v0.4.10-rc1→.prerelease == true./releases/latestcontinua apontando para a estavel anterior (ex.:v0.4.9) — omake_latest: falsedorelease.ymlgarante isso.- O
latest.jsonservido porreleases/latest/download/latest.jsonpermanece o da estavel: inalterado. Olatest.jsondo RC existe so como asset do proprio prerelease, nao e servido pelalatest, entao o updater Tauri dos apps instalados nao o enxerga. garra update(que so conhece/releases/latest,crates/garraia-cli/src/update.rs:9) nao oferece o RC.- Docker (
deploy.yml): a imagem sai so com0.4.10-rc1+ sha — semlateste sem0.4(odocker/metadata-actionnao deriva{{major}}.{{minor}}de prerelease, elatestso entra em default branch ou dispatch comlatest=true).
-
Precisou de
-rc2? Corte uma tag nova (v0.4.10-rc2) — nunca reutilizev0.4.10-rc1(clientes podem ter cacheado assets; ver Rollback). A versao do Cargo segue0.4.10.
Quando a estavel v0.4.10 for cortada, ai sim roda-se o assemble.py --write
(os fragmentos acumulados viram changelog), remove-se a secao [rcN] de
rascunho se existir, e segue-se o §1 normal.
2. Tag
git fetch origin main
git tag vX.Y.Z <sha-do-merge> # ou no HEAD de origin/main
git push origin vX.Y.Z
Caminho alternativo (usado nas v0.3.3/v0.3.4): workflow_dispatch.
Actions → Release → Run workflow em main com version=vX.Y.Z. O
softprops/action-gh-release cria o tag no commit do run. Atenção: um tag
criado assim nasce do GITHUB_TOKEN, que não dispara workflows de
tag-push — o deploy.yml (imagem ghcr) precisa então de dispatch manual com
tag=vX.Y.Z (passo 6 da verificação). Nesse modo a imagem sai com as tags
vX.Y.Z + latest + sha, sem as derivadas semver X.Y.Z/X.Y.
3. O que o tag dispara (automático)
| Workflow | Publica |
|---|---|
release.yml | Binários linux-x86_64/arm64, windows x86_64/arm64, macos-intel/arm64 + os archives correspondentes + pacotes Linux (.deb/.rpm/AppImage) + install.sh + install.ps1 + os instaladores desktop do Windows + SHA256SUMS e um <asset>.sha256 por asset (obrigatório: garra update lê o per-asset — crates/garraia-cli/src/update.rs) + o latest.json do updater desktop + uma attestation de proveniência por asset, numa GitHub Release com notas geradas |
deploy.yml | Imagem ghcr.io/michelbr84/garraia multi-arch (amd64+arm64), tags X.Y.Z, X.Y e sha |
Assets da release.yml, em detalhe:
| Asset | Origem | Obrigatório? |
|---|---|---|
garraia-{linux,macos}-{x86_64,aarch64}, garraia-windows-x86_64.exe | os 5 jobs de build principais | x86_64 sim; aarch64 best-effort |
garraia-windows-aarch64.exe | job build-windows-aarch64 | best-effort (wasmtime é Tier 3 nesse alvo) |
garraia-*.tar.gz / garraia-windows-{x86_64,aarch64}.zip | step Package archives | acompanha o binário correspondente |
garraia-linux-{x86_64,aarch64}.deb / .rpm | job package-linux (nfpm pinado, ADR 0015) | best-effort; aarch64 depende do binário aarch64 |
garraia-linux-x86_64.AppImage | job package-linux (appimagetool pinado) | best-effort |
install.sh, install.ps1 | copiados do repo | sim |
garraia-desktop-windows-x86_64.msi, …-setup.exe | job build-windows-installer | best-effort |
garraia-desktop-linux-x86_64.deb / .AppImage | job build-linux-desktop (bundler do Tauri via scripts/build-desktop-linux.sh) | best-effort; AppImage ~80-100MB (embute webkit2gtk) |
garraia-mobile-android.apk | job build-android-apk (Flutter, apps/garraia-mobile) | best-effort; assinado com os secrets ANDROID_KEYSTORE_* quando existem, senão com a keystore de debug do runner (o job avisa) |
SHA256SUMS + um <asset>.sha256 por asset | step Generate checksums | sim |
latest.json (updater do desktop Tauri) | step Generate latest.json (scripts/release/latest-json.py, #1568/#1590) | sim — fail-closed: sem nenhum artefato de updater com .sig o step recusa |
attestation de proveniência por asset (gh attestation verify) | step Attest build provenance (actions/attest-build-provenance@v4) | sim — ver §4.3 |
Por que os binários crus continuam publicados. garra update resolve o
asset por nome exato (update.rs:42-48) e exige o <asset>.sha256 irmão
(:127). Os archives são aditivos: trocá-los pelos binários quebraria o
auto-update de toda instalação já existente no momento em que ela pulasse para
essa versão. Não renomeie nem remova os assets crus.
Instaladores desktop, ARM64 do Windows, pacotes Linux e o APK são best-effort.
Os jobs build-windows-installer, build-windows-aarch64, package-linux,
build-linux-desktop e build-android-apk estão no needs: do job release mas deliberadamente
fora da condição if: — mesmo padrão do build-linux-arm64. Uma falha emite ::warning:: e a
release sai sem o asset correspondente, sem continue-on-error (proibido pelo
CLAUDE.md). Modo de falha conhecido: o ProductVersion do WiX é numérico de
três partes e não expressa prerelease semver, e ele vem da versao do Cargo
(o tauri.conf.json nao declara version, entao o bundler le
version.workspace do Cargo.toml), nao da tag git — uma versao de
Cargo com -rc faz o job do MSI falhar, nao o tag -rc por si so. O gating
absorve isso. O package-linux roda separado do
job release de propósito: nfpm e appimagetool são pinados por versão +
SHA-256, e uma falha deles não pode bloquear a release (ADR 0015).
Pré-releases: versões com alpha/beta/rc no nome são marcadas como
prerelease automaticamente.
Auto-updater do desktop Tauri (ativo desde a v0.4.8)
O updater do app desktop está ligado e publicado desde #1568 (PR #1586, 2026-10-07) e consertado de ponta a ponta pelo #1590 (2026-10-08):
tauri.conf.jsonaponta parareleases/latest/download/latest.jsoncom apubkeydo signer preenchida, ebundle.createUpdaterArtifactsliga a emissão dos.tar.gz/.zipde updater com a.sigao lado.- O step
Generate latest.jsondorelease.ymlmonta o arquivo a partir dos.sigestagiados (scripts/release/latest-json.py) e o publica como asset. O gerador é fail-closed: sem nenhum artefato de updater com.sig, o step recusa (exit 1) em vez de publicar umlatest.jsonvazio. - Plataformas ausentes são omitidas, não viram entrada vazia: um build best-effort que não saiu contribui menos assinatura, não uma entrada quebra.
Limite da primeira release com o updater (v0.4.8): o tag saiu antes do
#1590, então o latest.json publicado cobre só linux-x86_64 e
darwin-aarch64 — sem windows-x86_64 (os .nsis.zip/.msi.zip com .sig
não existiam ainda) e sem darwin-x86_64 (macOS Intel não buildou). Nas
releases seguintes o Windows entra no latest.json sempre que o job
best-effort do instalador Windows passar.
O caminho de atualização da CLI (garra update) é separado e não é
afetado por nada desta seção — ele segue no <asset>.sha256 por asset
(regra 15). Os .sig do updater Tauri e as attestations do GitHub são
canais distintos e ambos verificáveis: gh attestation verify para os
assets crus (§4.3), e o próprio updater do app para os artefatos assinados
com a chave do Tauri.
4. Verificar
- Actions: run
Releaseverde para o tag (com no máximo os jobs best-effort em vermelho: linux-arm64, macos-arm64, windows-aarch64, installer, package-linux). https://github.com/michelbr84/GarraRUST/releases/latestaponta para a versão nova, com todos os assets e seus.sha256, e os 5 nomes crus da release anterior presentes byte-idênticos (superfície dogarra update, regra 15). Olatest.jsondo updater desktop (o mesmo que o app consulta) deve listar as plataformas cujos jobs best-effort passaram — plataforma faltando ali = aquele build não saiu nesta release.- Attestations:
gh attestation verify garraia-linux-x86_64 -R michelbr84/GarraRUST --tag vX.Y.Z— o stepAttest build provenancedorelease.ymlassina cada asset com a identidade do workflow; verificar ao menos o binário principal de cada plataforma. Falhou = a release saiu sem a attestacao (o step roda antes doCreate Release, entao um run vermelho ali significa release sem assets novos). garra updatea partir da versão anterior encontra e instala a nova. Este é o teste que prova que os formatos novos continuaram aditivos.- Windows:
irm https://github.com/michelbr84/GarraRUST/releases/latest/download/install.ps1 | iexnuma máquina real; abrir um terminal novo e confirmargarraia --version(prova que o PATH persistiu no registro, não só na sessão). - Pacotes Linux (quando o
package-linuxpassou):docker run --rm -v $PWD:/pkg ubuntu:22.04 bash -c "apt-get update -q && apt install -y /pkg/garraia-linux-x86_64.deb && garraia --version";docker run --rm -v $PWD:/pkg fedora rpm -qip /pkg/garraia-linux-x86_64.rpm;chmod +x garraia-linux-x86_64.AppImage && ./garraia-linux-x86_64.AppImage --version. - Deploy da imagem: com tag criado via push, o run
Deploydispara sozinho; com release viaworkflow_dispatch, disparar Actions → Deploy comtag=vX.Y.Z(ver §2). Depoisdocker pull ghcr.io/michelbr84/garraia:<tag>. - Garra Mobile: baixar
garraia-mobile-android.apk, conferir o.sha256, instalar por sideload num Android 11+ e rodar a seção 1 e a seção Runtime dodocs/mobile-qa-checklist.md. Sem os secretsANDROID_KEYSTORE_*o APK não atualiza por cima de uma instalação assinada por outra chave — desinstale antes. - No dia seguinte, o
install-endpoints.ymlagendado deve estar verde — inclusive a sondarelease-cdn/install.ps1, que ficava vermelha por design enquanto nenhuma release publicava oinstall.ps1.
Rollback
Release ruim: apagar a Release e o tag (git push origin :refs/tags/vX.Y.Z),
corrigir via PR e cortar vX.Y.Z+1. Nunca reutilizar um tag já publicado —
garra update verifica SHA-256 e clientes podem ter cacheado os assets.