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.

Fern publish pipeline An API definition is read by Fern, which runs the TypeScript and Python generators in the public group and pushes packages to npm and PyPI. fern/definition API + generators.yml fern generate --group public npm registry TS SDK PyPI Python SDK version from output.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.

Two credentials are in play on every publish, and confusing them accounts for most first-release failures. The Fern token authorises generation — it lets the hosted generators run and produce the package. The registry token authorises upload — it lets npm or PyPI accept the artifact. A job with only one of them fails at a point that does not obviously name the missing credential, which is why an authentication error here so often gets misdiagnosed as a registry problem.

Two credentials, two stages The Fern token authorises generation on the hosted generators while the registry token authorises the upload, and a publish needs both. fern generate in CI hosted generators needs FERN_TOKEN npm · PyPI · Maven needs the registry token a job missing either one fails — and the error names the wrong stage

The other recurring failure is version collision, and it has one durable fix: never choose the published version by hand. Registries reject a re-publish of an existing version, so a release that reuses a tag, or a --version flag someone forgot to bump, fails after generation has already run. Deriving the version from the git tag makes the registry version and the release tag the same fact rather than two facts that must be kept in agreement.

One source for the version number Taking the published version from the git tag removes the class of failures caused by a hand-entered version that disagrees with the release. git tag v1.4.0 the only input ${GITHUB_REF_NAME#v} strips the prefix published 1.4.0 cannot disagree

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.

Treat the first publish of each package as a separate, deliberate exercise. Registries make the initial upload of a name special: scopes must exist, ownership must be established, and some registries apply different rules to a package’s first version than to its later ones. Doing that for four registries in the middle of a release is how a release becomes an afternoon. Publish a 0.0.1 placeholder for each package name well before you need it, confirm the credentials work end to end, and then the real release exercises a path that has already succeeded once.

Rotate publish credentials on a schedule, and scope them narrowly. An automation token with publish rights to your whole organisation is a large blast radius for a documentation-adjacent pipeline. Most registries support tokens scoped to a single package, which is exactly the scope a per-language release job needs. Scope them that way, record their expiry somewhere that alerts before the date, and rotate on a cadence rather than after an incident — an expired token discovered during a release is a self-inflicted outage that is entirely predictable.

Keep a dry run in the pipeline permanently. The --preview path builds the exact artifact and reports the version it would publish without uploading anything, which makes it a cheap standing check that the whole chain still works. Running it on every pull request that touches the spec or the generator configuration means credential expiry, a renamed package, or a generator that stopped resolving is discovered on an ordinary branch rather than at the moment you are trying to ship.

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.