Release pipeline
Como o tempest-react-sdk é publicado no npm — workflow tag-push automático, com fallback manual.
Visão geral
Local: GitHub Actions:
make release TAG=X.Y.Z
│
▼
┌─────────────────────────┐
│ scripts/release.sh: │
│ 1. branch release/vTAG │
│ 2. npm version TAG │
│ 3. fecha o CHANGELOG │
│ ([Unreleased] → │
│ [TAG] — data) │
│ 4. validate (lint + │
│ format + typecheck │
│ + test + build) │
│ 5. commit + tag local │
│ 6. push branch + tag │──────────► tag push triggers
│ 7. abre PR via gh │ .github/workflows/release-npm.yml
└─────────────────────────┘ │
▼
┌────────────────────────────┐
│ build (contents: read) │
│ 1. Checkout @ tag │
│ 2. tag == package.json? │
│ 3. npm ci + lint + format │
│ 4. Typecheck + tests │
│ 5. Build + npm pack │
│ smoke (contents: read) │
│ 6. Bundler, Node cru e │
│ Vitest contra o tarball │
│ publish (id-token: write) │
│ 7. npm publish <tarball> │
│ --provenance │
│ 8. read-back: registry │
│ serve a versão e o │
│ o dist-tag bate? │
│ release (contents: write) │
│ 9. GitHub Release da tag │
│ (notas = CHANGELOG, │
│ tarball anexado) │
└────────────────────────────┘
As três superfícies andam juntas: a git tag, a versão no npm e o GitHub
Release. O workflow falha se a tag não descrever a versão do package.json, e
falha se o registry não estiver servindo a versão sob o dist-tag dela — então "workflow
verde" significa de fato "publicado e visível".
Uma tag vX.Y.Z é a única coisa que publica — o disparo manual também roda sobre a tag, nunca sobre uma branch. Não há "publish via PR merge" — o merge do release PR é apenas para sincronizar main com package.json + RELEASES.md atualizados.
Comandos
make release TAG=0.1.5
Pipeline completo. Requer working tree limpo + tag inexistente local/remoto.
Bloqueia se CHANGELOG.md não mencionar [TAG] ou [Unreleased] (com prompt para forçar continuação).
make release TAG=0.1.5 DRY_RUN=1
Idêntico, mas para antes do push — você inspeciona a branch e o tag locais antes de continuar manualmente:
git push -u origin release/v0.1.5
git push origin v0.1.5
gh pr create --base main --head release/v0.1.5 --title "chore: release v0.1.5"
make release TAG=0.1.5 SKIP_VALIDATE=1
Pula a validação local (npm ci, lint, format-check, typecheck, test, build, pack dry-run). Use apenas em emergências — o CI vai validar novamente do zero.
make validate
Roda toda a validação local sem fazer release. Equivalente ao bloco de validação do CI.
make publish
Fallback manual. Requer NPM_TOKEN no ~/.npmrc (token com bypass 2FA) ou npm login interativo. Não dispara workflow — publish direto.
npm config set //registry.npmjs.org/:_authToken=npm_xxx... --location=user
npm run build
make publish
Sem token com bypass 2FA, npm exige OTP:
npm publish --access public --otp=123456
make releases
Lista todas as tags v*.*.* ordenadas por versão (mais recentes primeiro).
make releases-md
Regenera RELEASES.md a partir das git tags. Chamado automaticamente pelo scripts/release.sh após criar a tag.
make releases-check
Relatório de sincronia das três superfícies — uma linha por git tag, dizendo se a versão existe no npm e se a tag tem GitHub Release:
TAG NPM RELEASE STATUS
v0.24.0 ok ok sincronizado
v0.23.0 ok FALTA DESSINCRONIZADO
Só leitura, seguro de rodar sempre. Use antes e depois de um release.
make releases-sync / make releases-sync-dry
Cria os GitHub Releases faltantes para tags que já existem (backfill), com as notas vindas da seção correspondente do CHANGELOG.md. Tags que já têm Release são puladas — o script é idempotente e nunca reescreve um Release existente.
make releases-sync-dry # lista o que faria, sem criar nada
make releases-sync # cria de verdade
Necessário porque o publish no npm e a criação do Release passaram a andar juntos só a partir da v0.24.0: as tags anteriores existiam no git e no npm, mas sem Release no GitHub.
Notas de um backfill nunca herdam [Unreleased]
O scripts/changelog.mjs notes <versão> só cai no bloco [Unreleased] quando recebe --allow-unreleased (o que o workflow faz, para o caso de um release cortado antes de datar a seção). No backfill a flag não é passada — assim uma tag antiga nunca recebe as notas do ciclo seguinte; sem seção, o Release sai com um ponteiro para o CHANGELOG.md.
CI workflow (.github/workflows/release-npm.yml)
Disparado por:
push: tags: [v*.*.*]— fluxo principal.make release TAG=Xpush uma tag e o workflow dispara automaticamente.workflow_dispatch— manual, sempre sobre a tag:gh workflow run release-npm.yml --ref vX.Y.Z. Útil quando o publish de uma tag falhou e você quer re-rodar sem incrementar versão. Disparado de uma branch (--ref main), o guard do passo 3 reprova a run antes de instalar qualquer coisa — até a 0.76.0 ele derivava a tag dopackage.jsonda branch e publicava aquela árvore.
O workflow parte de permissions: {}, roda um release por vez, de qualquer tag (concurrency com grupo único release-npm, sem cancelar o que está em andamento — o publish dele pode já ter acontecido) e se divide em quatro jobs, cada um com só a permissão de que precisa. A divisão é o ponto: o único job que pode publicar não roda nada resolvido do registry na hora do release.
| Job | Permissão | O que roda |
|---|---|---|
build |
contents: read |
guard de versão, npm ci, lint, format, typecheck, testes, build, npm pack |
smoke |
contents: read |
os três smokes do tarball, com as instalações flutuantes do consumidor |
publish |
id-token: write (environment npm-release) |
npm CLI fixado + npm publish do tarball já testado + read-back |
release |
contents: write |
o GitHub Release, sem instalar nada |
Passos, na ordem:
- Checkout (
actions/checkout, fixado por SHA como toda action do workflow) comfetch-depth: 0epersist-credentials: false— oGITHUB_TOKENnão fica gravado no.git/configpara os passos seguintes. - Node 22 + cache npm.
- Guard de ref (
scripts/check-release-ref.sh) — aborta, antes de qualquer instalação, se o ref da run não é uma tagvX.Y.Z(ouvX.Y.Z-pre), se a tag não descreve oversiondopackage.json, ou se ela não aponta para o commit em checkout (GITHUB_SHA). Vale para tag push e paraworkflow_dispatch. Também derivaprerelease(versão com-) para marcar o Release corretamente, e escolhe o dist-tag comparando a versão com olatestatual do registry, que o passo anterior lê comscripts/release-dist-tag.sh current-latest(veja Dist-tags). npm ci, lint, format check, typecheck, testes e build.npm pack— o tarball sobe como artifactnpm-tarball-<tentativa>(um nome porrun_attempt, para um re-run completo não colidir com o artifact da primeira tentativa) e o SHA-256 dele vira output do job. Output de job não pode ser reescrito por job posterior, então todo job que baixa o artifact confere o digest antes de usá-lo (sha256sum --check). O artifact leva só o tarball: o workspace dobuildjá rodounpm ci, testes e build, e um arquivo dali pode ter sido reescrito por código de dependência — um digest tirado depois disso só atestaria a troca. Os jobspublishereleaserodamscripts/de um checkout próprio doGITHUB_SHA(persist-credentials: false; nopublish, checkout esparso só descripts/release-dist-tag.sh, sem instalação nenhuma depois) e baixam o artifact em$RUNNER_TEMP/npm-tarball, fora do checkout, para ele não sobrescrever nada.- Smoke (job
smoke) — instala o tarball em/tmp/sdk-smokecomreact@^19 react-dom@^19 react-router@^8e empacota com esbuild, roda as mesmas asserções em Node cru com onode-css-loaderpublicado, e roda uma suíte Vitest de consumidor (scripts/smoke-vitest.sh). Essas instalações resolvem versões flutuantes do registry — por isso moram num job sem permissão de escrita nenhuma. npm publish "./<tarball>" --provenance --access public(jobpublish) via Trusted Publishing (OIDC) — semNPM_TOKEN. Publica o arquivo que obuildempacotou e osmoketestou: publish de diretório re-rodaria oprepublishOnly(typecheck + lint + a suíte inteira + build) dentro do job comid-token, e mandaria um pack novo em vez do testado; publish de tarball não roda script de ciclo de vida nenhum. O--tagé escolhido de novo imediatamente antes do upload, relendo olatestdo registry com o mesmo script do guard — obuildleu minutos antes, e outro release pode tê-lo movido (veja Dist-tags). O npm CLI é instalado em versão exata (npm install -g npm@12.2.0), não@latest.- Read-back do registry — confirma que o npm serve
tempest-react-sdk@<versão>(até 30 minutos, porque o registry processa o upload de forma assíncrona) e que o dist-tag dela (latest,nextourelease-<major>.<minor>) aponta para ela — com a mesma janela de 30 minutos, porque o dist-tag atrasa tanto quanto a versão. Falha o job caso contrário: publish que aterrissou em outra tag deixaria de parecer verde. A espera do dist-tag só roda quando esta run fez o upload: num re-run o publish é pulado, não aplica tag nenhuma, e a run que publicou já esperou pela dela. - GitHub Release (job
release) —gh release create <tag>com o tarball do artifact anexado e as notas extraídas da seção doCHANGELOG.md(scripts/changelog.mjs notes <versão> --allow-unreleased), acrescidas do link para a versão no npm. Antes, confere que a tag ainda resolve para oGITHUB_SHApublicado e que o sha512 do tarball é odist.integrityque o npm serve (lido com a mesma janela de 30 minutos); ogh release createleva--verify-tag, então nunca cria uma tag faltante na HEAD da branch padrão. Se o Release já existe, fazgh release edit+upload --clobberem vez de falhar — e o--clobbersó re-sobe os mesmos bytes que o npm serve. Para recuperar uma run que publicou e falhou depois, use Re-run failed jobs: os jobs re-rodados herdam os outputs dobuilde baixam o artifact da tentativa que publicou. Um Re-run all jobs também roda (o artifact tem nome próprio), mas reempacota — e se o pack novo não for byte a byte o publicado, o jobreleasenão anexa nada e falha apontando paramake releases-sync(cria o Release sem asset) ou para anexar à mão o tarball do próprio registry (npm pack tempest-react-sdk@<versão>).
Por que o npm é fixado em 12.2.0
Trusted Publishing exige npm >= 11.5.1, e o Node 22 traz 10.x. Até a 0.76.0 o passo instalava npm@latest, e o latest mudou de major duas vezes debaixo do job sem ninguém escolher: os logs das runs da v0.74.0 à v0.76.0 mostram o npm 12 (npm notice run …, allowScripts). A 12.2.0 é a versão que publicou essas três releases. Subir o npm é uma mudança deliberada nessa linha, revisada como qualquer outra.
Dist-tags
O latest é monotônico: só anda para a frente. A regra mora num script só, scripts/release-dist-tag.sh, chamado em três lugares: pelo guard no job build, pelo job publish imediatamente antes do npm publish, e pelo job release para marcar o GitHub Release.
| Versão da tag | Dist-tag | Exemplo, com latest em 0.77.0 |
|---|---|---|
prerelease (X.Y.Z-algo) |
next |
v0.78.0-rc.1 → next |
estável, maior que o latest (ou sem latest) |
latest |
v0.78.0 → latest |
estável, igual ao latest (re-run da publicada) |
latest |
v0.77.0 → latest |
estável, menor que o latest |
release-<major>.<minor> |
v0.75.1 → release-0.75 |
Até a 0.77.0 toda versão estável ia para latest, então um backport v0.75.1 cortado depois da 0.77.0 viraria o default de todo npm install. Quem quer a linha antiga instala pelo dist-tag dela: npm install tempest-react-sdk@release-0.75. "Igual" só acontece no re-run de uma versão já publicada (o npm não aceita publicar a mesma versão duas vezes), e por isso fica em latest.
Como o latest é lido:
- Por
npm view <pacote> dist-tags --json, com a chave extraída em Node. Nuncanpm view <pacote> dist-tags.<nome>: o npm trata o ponto como caminho, edist-tags.release-0.75volta sempre vazio. - Só
E404explícito é pacote novo (semlatest). Resposta vazia com código 0, ou pacote que existe semlatest, reprova a run — tratar isso como pacote novo mandaria um backport paralatest. Outras falhas donpm viewsão retentadas 10 vezes, com 15 s entre elas, e depois reprovam.
Duas runs nunca disputam o latest: o workflow tem um grupo de concurrency único, então releases de tags diferentes entram em fila. E o job publish relê o latest segundos antes do upload; se ele mudou desde o build, o publish usa o dist-tag novo e deixa um ::notice::.
Fila do GitHub: só um pendente por grupo
O GitHub mantém uma run pendente por grupo de concurrency. Com um release rodando e duas tags enfileiradas, a terceira cancela a segunda enquanto ela ainda espera. A cancelada não publicou nada; dispare de novo com gh workflow run release-npm.yml --ref vX.Y.Z.
Backport precisa desta versão do workflow
Num tag push, o GitHub roda o release-npm.yml do commit da tag. Uma tag v0.75.1 cortada de uma branch que parte da 0.75.0 roda o workflow antigo, que manda tudo para latest. A branch de backport precisa ter o release-npm.yml, o scripts/check-release-ref.sh e o scripts/release-dist-tag.sh desta versão em diante (cherry-pick do fix da #472).
O GitHub Release segue o registry: o gh release create leva --latest quando o latest do npm é esta versão, e --latest=false caso contrário (backport, ou prerelease, que continua com --prerelease). O flag vem do registry, não do dist-tag escolhido, então um re-run que cria o Release de uma versão já publicada como latest ainda a marca. Release que já existe não tem o flag alterado: o re-run cai no gh release edit + upload --clobber, sem --latest.
Por que release-0.75 e não v0.75
O npm recusa dist-tag que seja range semver válido, e v0.75 é um (>=0.75.0 <0.76.0-0): npm publish --tag v0.75 sai com Tag name must not be a valid SemVer range: v0.75 (medido no npm 12.2.0, o que o job publish instala).
O script roda offline: o guard recebe CURRENT_LATEST por variável de ambiente (ausente, ele falha em vez de assumir pacote novo), e test/release-workflow.test.ts exercita cada linha da tabela num repositório git de fixture e cada resposta do registry com um npm falso no PATH.
Environment npm-release
O job publish roda no environment npm-release. Sem configuração, o GitHub o cria vazio e ele não barra nada — o workflow funciona igual. Ele é o gancho para dois controles que moram fora do repositório, e que continuam necessários:
- Em Settings → Environments →
npm-release: Required reviewers (o publish espera aprovação humana, mesmo depois de todos os gates verdes) e Deployment branches and tags restrito a tagsv*. - No Trusted Publisher do npmjs.com: o campo Environment preenchido com
npm-release. Daí em diante o npm só aceita a troca OIDC de um job nesse environment — outro workflow ou outro job do mesmo arquivo não publica.
Ligue os dois juntos
O revisor obrigatório sem o vínculo no npm deixa um job fora do environment publicar; o vínculo sem o revisor deixa qualquer run do environment publicar sem ninguém olhar.
O guard não confere se o commit passou por revisão
O check-release-ref.sh exige que a tag descreva o package.json e aponte para o commit da run, mas não que o commit esteja na main: o próprio scripts/release.sh empurra a tag da branch de release antes do merge, então publicar commit ainda não mergeado é o fluxo normal. O dist-tag monotônico impede que esse publish leve o latest para trás, não que ele aconteça. Quem barra commit não revisado é o revisor obrigatório no environment npm-release, junto com uma regra de tag v* (Settings → Rules → Rulesets) restringindo quem cria a tag — configuração do GitHub e do npm, a cargo do dono do repositório.
Segredos necessários no GitHub
Nenhum. O publish usa Trusted Publishing do npm: o npm publish troca a identidade OIDC do GitHub Actions por um token de vida curta, então não existe NPM_TOKEN no repositório. O que precisa estar configurado é um Trusted Publisher em npmjs.com apontando para este repositório + o arquivo release-npm.yml.
O GITHUB_TOKEN (usado para criar o Release) é fornecido automaticamente pelo Actions runtime — só o permissions: contents: write do job precisa estar declarado, e está.
O NPM_TOKEN continua sendo assunto do fallback local
make publish publica da sua máquina e aí sim precisa de token no ~/.npmrc (Classic Automation, ou Granular com "Allow bypass 2FA" marcado). Esse caminho não gera provenance e não cria o GitHub Release — se você usar o fallback, rode make releases-sync depois para o Release não ficar faltando.
Provenance signing
O publish inclui --provenance quando rodado no CI. Isso requer:
permissions: id-token: writeno workflow (já configurado).- npm >= 11.5.1 no runner (o job
publishinstala a 12.2.0, fixada). - Um Trusted Publisher configurado no npmjs.com para este repositório.
O resultado: cada versão publicada carrega um attestation assinado pelo sigstore, ligando o tarball ao commit + workflow run que o produziram. Visível no registry como badge "Verified provenance".
Publish manual local não consegue provenance — não há OIDC provider fora do CI. make publish sempre roda sem --provenance.
Histórico
Veja RELEASES.md (auto-gerado via make releases-md) e CHANGELOG.md (escrito à mão antes de cada release).
Veja também
CHANGELOG.md— registro de mudanças por versãoRELEASES.md— tabela de tags com data e commitMakefile— definição dos alvosscripts/release.sh— script bash do pipeline