Konec npm tokenů: publikujte balíčky bez secrets přes GitHub Actions

Publikování npm balíčků pomocí dlouhodobých tokenů uložených v GitHub Secrets je pohodlné, ale riskantní. Od léta 2025 nabízí npm elegantnější řešení: Trusted Publishers s OIDC autentizací, které tokeny zcela nahrazují. Žádné secrets, žádná rotace, žádný únik přihlašovacích údajů z logů. Ukážeme si, jak vše nastavit za pár minut.
Jak dnes vydáváte své npm balíčky? Obvykle asi přes npm publish. Možná používáte nějaký tool, jako je np, release-it, changesets nebo release-please od Google.
Ale věděli jste, že celý proces dnes můžete bezpečně svěřit GitHub Actions? A to bez rizika, že by váš npm token někam unikl – bez rotace, bez secrets, bez panických ataků? Od léta 2025 totiž funguje npm Trusted Publishers a nahrazují dlouhodobé tokeny krátkodobou OIDC autentizací mezi GitHub Actions a npm registrem.
V tomhle článku si ukážeme, jak celé řešení nasadit za pár minut, proč je každý řádek workflow napsaný právě takhle, a kam šlápnout, abyste si nezpůsobili 422 nebo 404 zbytečně. Ukážeme si to jak pro npm, tak pro Bun a pnpm.
Co je trusted publishing a proč o něm chcete vědět
Klasický automatizovaný publikační flow vypadal tak, že jste si na npmjs.com vygenerovali automation token, uložili ho do NPM_TOKEN jako GitHub Secret a v CI workflow pak přes NODE_AUTH_TOKEN předávali tenhle token do npm publish. Tohle samozřejmě funguje, ale má několik stinných stránek. Token je dlouhodobý – npm vám sice nabízí expirace 30, 60 nebo 90 dní, ale taky si můžete v klidu nastavit vlastní datum, třeba 5. dubna 2063.
Trusted publishing celý problém otáčí naruby. Místo aby vy poslali npm token, GitHub Actions runner se npm registru prokáže svojí identitou přes OpenID Connect. npm registr ověří, že daný workflow opravdu běží v tom repozitáři a souboru, který jste si u balíčku zaregistrovali, a vydá jednorázový krátkodobý credential platný jen po dobu publikace. Žádný klíč tedy nikdy neopustí GitHub infrastrukturu, žádné secrets v repu, žádná rotace.
Jako bonus dostanete provenance attestation – podepsaný záznam přes Sigstore, který spojuje publikovaný balíček s konkrétním commitem, workflow runem a build prostředím na GitHubu. Podpis se zároveň ukládá do veřejného transparency logu Rekor, takže ho nikdo (ani npm) nemůže potichu změnit.

Jako bonus dostanete provenance attestation, což je kryptografický podpis přes Sigstore, který spojuje váš publikovaný balíček s konkrétním commitem a workflow runem na GitHubu. Na npmjs.com se u balíčku objeví zelená značka s odkazem na zdrojový kód a uživatelé si můžou ověřit, že to, co npm install stáhne, opravdu vzniklo z toho, co je na GitHubu.
Tohle všechno má největší hodnotu právě v době, kdy supply-chain útoky jako ten na polyfill.io ukazují, že npm ekosystém není zcela bezpečné místo.
Jediný háček je, že už nelze publikovat balíček lokálně z příkazové řádky – publikace musí proběhnout přes GitHub Actions. Podle mě to ale pro většinu projektů není omezení, spíš naopak bonus.
Setup ve třech krocích
1. Připravte package.json
Pole repository musí přesně odpovídat vašemu GitHub repozitáři! Tohle je dost zásadní, protože npm i Sigstore tuhle URL při publikaci validují a při neshodě vám pošlou 422 Unprocessable Entity. Zvlášť pozor na velká a malá písmena v názvu organizace nebo uživatele:
{
"name": "muj-balicek",
"version": "1.0.0",
"repository": {
"type": "git",
"url": "git+https://github.com/FrontEndDev-org/muj-balicek.git"
},
"publishConfig": {
"access": "public"
}
}
Code language: JSON / JSON with Comments (json)
Dále je nutné nastavit "access": "public" u scoped balíčků (@org/package) při prvním publikování, jinak vám npm s odkazem na placený plán řekne, že soukromý balíček publikovat nemůžete.
2. Nakonfigurujte trusted publisher na npmjs.com
Tady narazíte na chicken-and-egg problém: balíček musí na npm registru už existovat, abyste mu mohli nastavit trusted publisher. Pro úplně nový balíček tedy uděláte úvodní verzi 0.0.1 ručně klasickým přes npm publish. Tohle je poslední lokální publish v životě vašeho balíčku.

Jakmile balíček existuje, na adrese https://www.npmjs.com/package/VAS-BALICEK/access najdete v Settings sekci Trusted Publisher. Vyberte GitHub Actions a vyplňte:
- Organization or user — váš GitHub username nebo název organizace
- Repository — jen název repa, bez
username/předpony - Workflow filename — přesný název souboru, např.
publish.yml - Environment (volitelné) — pokud používáte GitHub Actions environment
Všechno je case-sensitive a npm konfiguraci při uložení nevaliduje. Pokud máte organizaci FrontEndDev-org a napíšete frontenddev-org, uložení projde, ale při prvním publikování dostanete nicneříkající 404 a budete půl hodiny tápat – co sakra děláte špatně? Mluvím z vlastní hořké zkušenosti – že @OzzyCzech 🤦♂️

3. Workflow .github/workflows/publish.yml
Název souboru se musí přesně shodovat s tím, co jste zadali v npm konfiguraci:
name: Publish to npm & Create Release
on:
push:
tags:
- "v*.*.*" # kdy se má workflow spustit
workflow_dispatch:
# vypnutí výchozích oprávnění a explicitní
# povolení jen v jobu, který je potřebuje
permissions: {}
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write # potřeba pro GitHub Release
id-token: write # potřeba pro OIDC autentizaci
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "25"
registry-url: "https://registry.npmjs.org"
# instaluje přesně podle package-lock.json
# a buildy jsou tak deterministické
- run: npm ci
# build a testujeme (pokud máme)
- run: npm run build
- run: npm test
# publikujeme a přidáme provenance
- name: Publish to npm
run: npm publish --provenance --access public --ignore-scripts
# vytvoří GitHub Release s release notes
# release notes se generují z commit
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
generate_release_notes: true
Code language: YAML (yaml)
Publikace nové verze pak vypadá takhle:
npm version patch # nebo minor / major
git push origin main --tags # push tagu spustí workflowCode language: Shell Session (shell)
npm version zvedne verzi v package.json, vytvoří commit, otaguje ho — a push tagu spustí workflow. Žádné ruční kliknutí v GitHub UI, žádné kopírování verzí mezi soubory.
Proč je workflow napsaný právě takhle
Každý řádek tam má svůj důvod a vyplatí se je probrat, protože až narazíte na chybu, budete vědět, kde hledat:
permissions: {} na úrovni workflow vypne všechna výchozí oprávnění a explicitně je povolíme jen v jobu, který je potřebuje. Princip minimálních privilegií — kdyby vám někdo někdy injektnul škodlivý step, nebude mít přístup k zápisu kamkoli, kam nemusí.
id-token: write je nejdůležitější řádek celého workflow. Bez něj GitHub Actions nevygeneruje OIDC token a npm vás nedokáže autentizovat. Chyba bývá zavádějící — typicky 404 nebo „authentication required“ — takže pokud něco neklape, podívejte se sem nejdřív.
npm ci místo npm install instaluje přesně podle package-lock.json a buildy jsou tak deterministické. Vyžaduje to mít lockfile commitnutý v repu — pokud ho ignorujete v .gitignore – musíte ho commitnout.
registry-url v setup-node je povinný, přestože registry.npmjs.org je stejně default. Bez tohohle parametru akce nevytvoří .npmrc soubor potřebný pro OIDC autentizaci. Vypadá to redundantně, ale není – fakt to tam musí být.
Node 25 kvůli novějšímu npm s podporou OIDC. Minimum je npm ≥ 11.5.1, což odpovídá Node ≥ 24. Pokud z nějakého důvodu musíte zůstat na starší verzi Node, přidejte npm install -g npm@latest před publikováním.
--provenance flag je technicky volitelný — provenance se může generovat i automaticky. Doporučuju ho ale uvádět explicitně. Někteří uživatelé hlásí, že bez něj při prvním publikování provenance nevznikne. Je lepší si o provenance explicitně říct.
Žádný NODE_AUTH_TOKEN — a tohle je důležité! Pokud máte v repozitáři starší NPM_TOKEN secret z předchozího setupu a v workflow ho někde nastavujete, smažte ho. I prázdná proměnná NODE_AUTH_TOKEN="" způsobí, že npm CLI se pokusí o tokenovou autentizaci místo OIDC a celé to spadne na podivné chybě.
Používáte Bun? Žádný problém
Pokud jste si oblíbili Bun jako rychlejší package manager (a po přečtení článku Docker + Bun + Caddy by to nebylo překvapení), nemusíte na trusted publishing rezignovat. Stačí přidat oven-sh/setup-bun před setup-node. Samotná publikace pořád běží přes npm publish — OIDC autentizace je totiž navázaná na .npmrc, který vytvoří akce setup-node:
steps:
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v6
with:
node-version: "25"
registry-url: "https://registry.npmjs.org"
- run: bun install --frozen-lockfile
- run: bun run build
- run: bun run test
# jo bun zatím neumí OIDC autentizaci :facepalm:
# https://github.com/oven-sh/bun/issues/15601
- name: Publish to npm
run: npm publish --provenance --access public --ignore-scripts
Code language: YAML (yaml)
--frozen-lockfile je Bun ekvivalent npm ci a vyžaduje commitnutý bun.lockve vašem repozitáři.
A pro pnpm fanoušky
Pro pnpm je setup analogický. Přidáte pnpm/action-setup před Node a v setup-node můžete navíc zapnout cache na pnpm:
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: "25"
registry-url: "https://registry.npmjs.org"
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- run: pnpm test
- name: Publish to npm
run: npm publish --provenance --access publicCode language: PHP (php)
Vyžaduje to commitnutý pnpm-lock.yaml, jinak vám --frozen-lockfilespadne.
Alternativní trigger: GitHub Release
Někomu může víc vyhovovat vytvářet release ručně přes GitHub UI a publikaci na to navázat. Stačí přepnout trigger:
on:
release:
types: [published]
permissions: {}
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "25"
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm publish --provenance --access publicCode language: PHP (php)
Výhoda je, že máte release notes hotové předem a můžete si je v UI v klidu projít. Nevýhoda — víc ručních kroků.
Na co si dát pozor
Pár věcí, které vás zaručeně přijdou drahé, pokud na ně nedáte pozor předem:
NODE_AUTH_TOKENze starého setupu – i prázdný způsobí selhání. Smažte ho ze secrets i z workflow.- Chybějící
id-token: write– bez něj OIDC token nevznikne a chybová hláška vás nezachrání. - Case sensitivity všeho – username, název repa i název workflow souboru musí přesně sedět. Organizace s velkými písmeny dokážou rozbít i samotné ověření provenance.
- Self-hosted runnery nefungují – trusted publishing vyžaduje GitHub-hosted runnery. Pokud máte vlastní infrastrukturu, zůstaňte u tokenů nebo ruční publikace.
- Jeden trusted publisher na balíček – pro víc workflow (třeba prod a beta kanál) musíte udělat jeden vstupní workflow, který deleguje na reusable workflows.
- Reusable workflows –
id-token: writemusí být povolené jak na volajícím, tak na volaném workflow. Nestačí to dát jen na jedno místo. - Privátní repozitáře – provenance se v nich negeneruje (není kam odkázat), samotná OIDC autentizace ale funguje normálně.
Jak ověřit, že to funguje
Po prvním úspěšném publikování přes trusted publisher si zajděte na stránku balíčku na npmjs.com – měla by tam svítit zelená značka „Provenance“ s odkazem na konkrétní commit a workflow run. Lokálně si můžete podpisy zkontrolovat příkazem npm audit signatures (nebo bun audit signatures pro Bun):
npm audit signatures
Pokud jste paranoidní (nebo jen rozumně opatrní), na npmjs.com v nastavení balíčku zapněte „Require two-factor authentication and disallow tokens“. Tím zablokujete jakékoli publikování přes tokeny – i kdyby vám někdo ukradl npm heslo a přidal si automation token, publikovat nebude moct. Jediná povolená cesta zůstane přes nakonfigurovaný trusted publisher z konkrétního workflow ve vašem repozitáři.
Závěrem
Trusted publishing není revoluce – je to evoluce, která posouvá npm tam, kde ostatní package registry (PyPI, RubyGems, crates.io) už chvíli jsou. Ale pro vás, kdo udržujete byť jen jeden veřejný balíček, je to konec jedné nepříjemné rutiny: žádné rotace, žádné secrets, žádné panické přepublikovávání po úniku tokenu z logů. A jako bonus máte na svém balíčku zelenou značku, která uživatelům dává jistotu, že to, co npm install stahuje, opravdu pochází z toho repozitáře, který je v package.json.
Pokud spravujete víc balíčků, nastavte si trusted publisher pro všechny – investice patnácti minut vám ušetří hodiny budoucího rotování secrets a jednoho dne možná i ošklivé probuzení po bezpečnostním incidentu.
Zdroje: