Publicar um pacote quase nunca é um comando só na sua máquina. Normalmente a gente deixa isso a cargo do CI — um servidor que roda tarefas automáticas cada vez que você envia código (aqui vamos usar o GitHub Actions, o CI que vem embutido no GitHub). E, por anos, a receita foi: gerar um token de publicação (uma senha longa que autoriza subir o pacote) e guardá-lo como secret — um segredo salvo nas configurações do repositório.
Funciona, mas é uma bomba-relógio. Esse token é uma chave que fica guardada: se ela vaza num log, num fork do projeto ou numa dependência maliciosa, qualquer um passa a publicar no seu lugar. Tanto que o cerco está fechando — o npm já anunciou que vai restringir tokens que burlam o 2FA (para contas em agosto/2026 e para publicação direta em janeiro/2027).
A alternativa moderna é o Trusted Publishing (publicação confiável) usando OIDC (OpenID Connect), um padrão de identidade. A ideia, numa analogia: em vez de deixar uma chave-mestra guardada embaixo do tapete, o seu robô de publicação chega na portaria e mostra um crachá temporário que o próprio GitHub emite na hora. O porteiro (o PyPI ou o npm) confere se aquele crachá é mesmo daquele projeto e daquele processo — e só então deixa publicar. Nada de segredo salvo em lugar nenhum.
Neste guia você vai configurar do zero os dois lados — PyPI (o registro oficial de pacotes Python) e npm (o do JavaScript) — e, no fim, publicar vira só criar uma tag de versão. Vou incluir de propósito todos os perrengues que enfrentei na prática, pra você pular direto para a parte que funciona. 🚀
Como o "crachá" (OIDC) substitui o token
Toda tarefa no GitHub Actions roda dentro de um job (um passo do processo). Quando você dá a esse job a permissão id-token: write, o GitHub emite um crachá de identidade — um token curto, assinado e que expira em minutos — descrevendo exatamente de onde aquele job saiu: a organização, o repositório e o arquivo de workflow (a receita de automação). O registro de pacotes foi configurado, do lado dele, para confiar só nessa combinação específica. A ferramenta de publicação troca esse crachá por uma permissão temporária e sobe o pacote. Um fork, outro workflow ou um token vazado simplesmente não conseguem forjar o crachá.
O que você vai precisar
- Seu código num repositório do GitHub, com o Actions ligado.
- Um workflow de release — o arquivo de receita que roda quando você cria uma tag (um marcador de versão no Git, ex.:
v0.0.1). - A permissão
id-token: writeno job — é ela que libera a emissão do crachá. - Contas no PyPI e/ou no npm com 2FA (verificação em duas etapas) ativo.
Parte 1 — PyPI (Python)
O PyPI tem um detalhe que facilita muito a vida: ele aceita um pending publisher (algo como "publicador pendente"). Ou seja, você cadastra a confiança antes mesmo do pacote existir no site. A primeira tag que você enviar já cria o projeto e publica de uma vez — sem precisar de um primeiro envio manual, como veremos que o npm exige.
1. Registrar o pending publisher
Em pypi.org → Account settings → Publishing → Add a pending publisher, escolha GitHub e preencha:
- PyPI Project Name — o nome do pacote no PyPI (ex.:
mcp-gtw). - Owner — a organização ou usuário do GitHub (ex.:
mcp-gtw). - Repository name — o nome do repositório.
- Workflow name — só o nome do arquivo (ex.:
release.yml). - Environment name — deixe em branco, a menos que use GitHub Environments.
💡 O nome de distribuição no PyPI pode ser diferente do nome de importação. Se o nome desejado já estiver ocupado, publique com outro (ex.: meu-pkg) e mantenha o import meu_pacote — só ajuste o importlib.metadata.version("nome-de-distribuicao") para bater com o nome publicado.
2. O workflow de release
Esse é o arquivo da receita, que fica em .github/workflows/release.yml no seu repositório. Repare no que não tem: nenhuma linha de env com segredo, nenhum PYPI_API_TOKEN. Só a permissão id-token: write (o crachá) e o parâmetro --trusted-publishing always. Aqui uso o uv para build e publish, mas o action oficial pypa/gh-action-pypi-publish funciona igual:
name: release
on:
push:
tags: ["v*"]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Install uv
run: pip install uv
- name: Build
run: uv build
- name: Publish to PyPI
run: uv publish --trusted-publishing always
3. Publicar
Suba a versão, crie a tag e empurre — o CI builda e publica sozinho:
git tag v0.0.1
git push origin v0.0.1
Em segundos o pacote aparece em https://pypi.org/project/seu-pacote/. No meu caso, o PyPI subiu de primeira. ✅
Parte 2 — npm (JavaScript)
No npm a ideia é a mesma, mas o caminho tem duas pegadinhas que me custaram várias tentativas frustradas. Vou te entregar já resolvidas, pra você não perder o tempo que eu perdi.
1. Versões de Node e npm
O suporte a Trusted Publishing é recente, então ele exige npm ≥ 11.5.1 e Node ≥ 22.14. O problema: nem o Node 22 ou 24 já vem com um npm tão novo por padrão. A saída é atualizar o npm dentro do próprio workflow, antes de publicar:
npm install -g npm@latest
Sem isso, o npm nem tenta o OIDC — cai direto para token e falha.
2. Pacote novo? O primeiro publish é manual
Diferente do PyPI, o npm não deixa pré-configurar um pacote que ainda não existe: a tela de Trusted Publisher só aparece depois que o pacote existe. Então, só na estreia, crie o pacote com um publish manual:
npm login
npm publish --access public
3. As DUAS partes da configuração (aqui mora o perrengue)
Na página do pacote em npmjs.com → Settings existem duas seções separadas, e você precisa das duas:
- Trusted Publisher → em Select your publisher, clique em GitHub Actions e preencha organização, repositório e workflow (
release.yml), deixe o environment vazio, marque Allow npm publish e clique em Set up connection. - Publishing access → marque Require two-factor authentication and disallow tokens (recommended) e salve.
⚠️ O erro que eu cometi: configurei só a segunda parte (proibir tokens) e achei que tinha acabado. Como os tokens estavam proibidos e não havia trusted publisher configurado, não existia forma de autenticar — e todo publish falhava com 404 Not Found ou ENEEDAUTH. A lição: configure a conexão de Trusted Publisher primeiro. A opção de "disallow tokens" sozinha só fecha a porta dos tokens.
4. O workflow de release
name: release
on:
push:
tags: ["v*"]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
- name: Upgrade npm for trusted publishing
run: npm install -g npm@latest
- name: Install
run: npm ci
- name: Publish
run: npm publish --access public
5. Um mito desfeito: o registry-url não é o vilão
Durante a depuração, o registry-url do setup-node parecia culpado: ele escreve um .npmrc com um _authToken vazio, e cheguei a achar que isso bloqueava o OIDC (o erro virou ENEEDAUTH quando o removi). Era pista falsa. O exemplo oficial do npm usa o registry-url, e o npm ≥ 11.5.1 tenta o OIDC antes de cair para token. A causa real dos dois erros era sempre a mesma: o trusted publisher não estava configurado. Mantenha o registry-url e configure a conexão no site.
Conferindo o release
Com o gh CLI você acompanha o run e, se falhar, lê só o passo que quebrou:
gh run list --workflow=release.yml --limit 1
gh run view <run-id> --log-failed
E confirma direto nos registries:
npm view seu-pacote version
curl -s https://pypi.org/pypi/seu-pacote/json | jq .info.version
💡 O endpoint JSON do PyPI tem cache de alguns minutos: logo após publicar, ele pode ainda mostrar a versão anterior mesmo com o upload já concluído. O log do job (Uploading ...whl) é a fonte da verdade.
Os perrengues, em forma de checklist
Se algo der errado, é bem provável que seja um destes — foi o que travou comigo:
- Nome já ocupado no PyPI → escolha outro nome de distribuição (ele pode ser diferente do nome que você usa no
import) e ajuste oversion("nome")para bater. - npm respondendo 404 ou ENEEDAUTH → quase sempre é o trusted publisher que ficou sem configurar (marcar só "disallow tokens" não basta).
- npm ignorando o crachá (OIDC) → npm velho demais; force o
npm install -g npm@latestno workflow. - Pacote novo no npm → faça um
npm publishmanual só para criar o pacote, e só depois configure o trusted publisher. - Nenhuma autenticação acontece → provavelmente faltou a permissão
id-token: writeno job.
Lançando novas versões
Do segundo release em diante é sempre o mesmo, para os dois pacotes, sem nunca mais tocar em token:
make version v=X.Y.Z # sobe a versao no pyproject.toml / package.json
git tag vX.Y.Z
git push origin main vX.Y.Z
O CI valida a tag contra a versão do projeto, roda os testes, builda e publica via OIDC.
Conclusão
Trusted Publishing via OIDC troca "um segredo guardado que pode vazar" por "uma identidade efêmera e verificável". Depois da configuração inicial — pending publisher no PyPI e a conexão de trusted publisher + disallow tokens no npm — publicar vira só empurrar uma tag. Menos secrets para gerenciar, menos superfície de ataque, e o release fica auditável de ponta a ponta. Vale cada minuto da configuração. 🔐✨
Links úteis: PyPI Trusted Publishers · npm Trusted Publishers · OIDC no GitHub Actions · Deprecação de tokens 2FA-bypass do npm