Publishing SDKs to npm and PyPI with Fern
This guide shows how to publish Fern-generated TypeScript and Python SDKs straight to npm and PyPI from a single generators.yml, including registry tokens, generator groups, and version bumping. It is part of the Fern SDK generation section and the broader SDK Generation & Changelog Automation framework. By the end you will have one fern generate --group public command that builds both clients and pushes them to their registries with the correct semantic version.
Problem & Context
Fern can generate idiomatic SDKs, but a generated dist/ folder is useless to your users until it lands in a package registry. The friction is that npm and PyPI have different auth models, different versioning conventions, and different “already published” failure modes — and most teams wire up two separate release scripts that drift apart. Fern collapses this into the output block of each generator: you declare location: npm or location: pypi, point at a token, set a version, and Fern handles the build-and-publish for you.
The other recurring problem is accidentally publishing the wrong languages. Without grouping, fern generate runs every generator you have configured. Generator groups let you run exactly the set you mean — a public group for the SDKs you ship and, say, a separate local group for SDKs you only generate into the repo for testing. This guide assumes you already have a Fern folder initialized (fern init) with an API definition; if you need to set up several languages at once, see configuring Fern for multiple languages.
Step-by-Step Solution
1. Install the Fern CLI
Pin the CLI so generator versions stay reproducible across machines and CI.
npm install -g [email protected]
fern --version
Expected output:
0.64.21
2. Declare npm and PyPI generators in generators.yml
Open fern/generators.yml. Add the TypeScript Node SDK generator with an npm output and the Python SDK generator with a pypi output, both under a group named public. Pin every generator version explicitly.
# fern/generators.yml
groups:
public:
generators:
- name: fernapi/fern-typescript-node-sdk
version: 0.50.0
output:
location: npm
package-name: "@acme/acme-sdk"
token: ${NPM_TOKEN}
version: 1.4.0
config:
namespaceExport: AcmeApi
- name: fernapi/fern-python-sdk
version: 4.18.0
output:
location: pypi
package-name: "acme-sdk"
token: ${PYPI_TOKEN}
version: 1.4.0
The package-name is the published registry name (scoped for npm, PEP 503-normalized for PyPI). token is interpolated from the environment, so no secret ever lives in the file.
3. Validate the configuration
Catch typos before you attempt a publish, which is far cheaper than a half-published release.
fern check
Expected output:
fern/generators.yml: All checks passed
4. Export the registry tokens
npm needs an automation token (created at npmjs.com under Access Tokens → Granular/Automation). PyPI needs a project-scoped API token (created under Account settings → API tokens); its value always begins with pypi-.
export NPM_TOKEN="npm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export PYPI_TOKEN="pypi-AgEIcHlwaS5vcmcxxxxxxxxxxxxxxxxxxxxx"
For PyPI, Fern publishes with the username __token__ automatically when location: pypi is used; you only supply the token value.
5. Bump the version
Pick one source of truth for the version. Either edit output.version in generators.yml, or override it at publish time for both generators at once:
fern generate --group public --version 1.4.1
The --version flag wins over output.version, so use it for one-off releases and keep generators.yml for the committed baseline.
6. Generate and publish
Run the group. Fern builds each SDK in an isolated container and pushes it to the registry named in its output.location.
fern generate --group public
Expected output:
[fern-typescript-node-sdk]: Building @acme/[email protected]
[fern-typescript-node-sdk]: Published @acme/[email protected] to npm
[fern-python-sdk]: Building acme-sdk 1.4.1
[fern-python-sdk]: Published acme-sdk 1.4.1 to pypi
┌─
│ ✓ fernapi/fern-typescript-node-sdk
│ ✓ fernapi/fern-python-sdk
└─
Confirm the releases landed:
npm view @acme/acme-sdk version
pip index versions acme-sdk
Complete Working Example
A single CI workflow that bumps the version from the Git tag, installs Fern, and publishes both SDKs. Save it as .github/workflows/publish-sdks.yml.
# .github/workflows/publish-sdks.yml
name: Publish SDKs
on:
push:
tags:
- "v*.*.*"
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Fern CLI
run: npm install -g [email protected]
- name: Validate Fern configuration
run: fern check
- name: Derive version from tag
id: ver
# v1.4.1 -> 1.4.1
run: echo "value=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Generate and publish SDKs
run: fern generate --group public --version "${{ steps.ver.outputs.value }}"
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
With the matching generators.yml from step 2, pushing a tag like git tag v1.4.1 && git push origin v1.4.1 publishes @acme/[email protected] to npm and acme-sdk 1.4.1 to PyPI in one run.
Gotchas & Edge Cases
Republishing an existing version fails hard. Both registries reject a version that already exists, and Fern surfaces this as a non-zero exit with 403 You cannot publish over the previously published versions. Always bump the version before re-running; deriving it from a Git tag (as in the example) makes duplicates impossible because tags are unique.
PyPI normalizes package names. A package-name of Acme_SDK is published and installed as acme-sdk per PEP 503. Reference the normalized form in pip install and in pip index versions, or the lookup returns nothing even though the publish succeeded.
npm scoped packages default to private. If @acme/acme-sdk is a new scoped package on a free org, the first publish fails with 402 Payment Required unless you mark it public. Add "publishConfig": { "access": "public" } — Fern lets you inject this via the generator’s config for the TypeScript generator, or set the scope to public in your npm org settings.
FAQ
How does Fern decide the version of the published package?
Fern reads the version from the output.version field in generators.yml, or from the --version flag passed to fern generate. If neither is set, Fern publishes the version already declared in the generator block and will fail on a duplicate version that already exists in the registry.
Do I need separate registry tokens for npm and PyPI?
Yes. npm uses an automation token stored as NPM_TOKEN and PyPI uses an API token stored as PYPI_TOKEN. Fern reads each token from the environment variable referenced in the generator’s output.token, so both must be present when you publish to both registries in one run.
Can I publish only one language without regenerating the others?
Yes. Group your generators in generators.yml and run fern generate --group public to publish every generator in that group, or target a single language by placing it in its own group. Generators outside the named group are skipped entirely.
Related
- Fern SDK generation — parent section for all Fern configuration topics.
- Configuring Fern for multiple languages — set up TS, Python, Java, and Go generators in one file.
- SDK Generation & Changelog Automation — the overarching framework for generating and releasing SDKs.
- Automating SDK releases with the Speakeasy GitHub Action — an alternative release pipeline for spec-driven SDKs.