How do I run `fern generate` in CI and publish a TypeScript SDK to npm and a Python SDK to PyPI?
API SDK & Docs Platforms

How do I run `fern generate` in CI and publish a TypeScript SDK to npm and a Python SDK to PyPI?

7 min read

Shipping SDKs automatically from CI is one of the fastest ways to keep your TypeScript and Python clients in sync with your API. With Fern, you can generate both SDKs from the same API definition and publish them to npm and PyPI on every tagged release (or on demand) using a simple CI workflow.

This guide walks through how to run fern generate in CI and publish a TypeScript SDK to npm and a Python SDK to PyPI, focusing on a GitHub Actions setup. The same principles apply to other CI providers.


Prerequisites

Before wiring this into CI, make sure you have:

  • A Fern workspace (e.g. a fern/ directory with your API definition and fern.config.json).
  • Generation targets configured for:
    • TypeScript SDK
    • Python SDK
  • Publishing access:
    • npm access token with publish permission
    • PyPI (or TestPyPI) API token
  • A CI provider (examples below use GitHub Actions).

Example fern.config.json

Your fern.config.json (or fern.config.yml) should define the SDK generators you want to run:

{
  "api": {
    "workspace": "fern"
  },
  "groups": {
    "sdks": {
      "generators": [
        {
          "name": "fernapi/fern-typescript-sdk",
          "version": "0.x.x",
          "output": {
            "location": "local-file-system",
            "path": "generated/typescript"
          },
          "config": {
            "npmPackage": "@your-org/your-typescript-sdk",
            "namespaceExport": "YourSdk"
          }
        },
        {
          "name": "fernapi/fern-python-sdk",
          "version": "0.x.x",
          "output": {
            "location": "local-file-system",
            "path": "generated/python"
          },
          "config": {
            "packageName": "your_python_sdk"
          }
        }
      ]
    }
  }
}

This configuration tells fern generate to produce both SDKs locally in the repo, which your CI job will then build and publish.


Installing Fern in CI

You can either:

  1. Install Fern globally via npm, or
  2. Use npx to run it without a global install.

For CI, npx is usually simplest:

npx fern-api/fern-cli@latest generate --group sdks

Or, if you’ve added fern to your devDependencies:

npm install --save-dev fern-api/fern-cli
npx fern generate --group sdks

Using a generator group (like sdks) ensures you run only the SDK-related generators in CI.


Running fern generate in GitHub Actions

Below is a GitHub Actions workflow that:

  1. Triggers on Git tags.
  2. Runs fern generate to produce both SDKs.
  3. Builds and publishes:
    • TypeScript SDK to npm
    • Python SDK to PyPI

Step 1: Add secrets to your repository

In your GitHub repository settings, define:

  • NPM_TOKEN – npm access token with publish scope
  • PYPI_API_TOKEN – PyPI API token (e.g. pypi-***)

For TestPyPI, you might instead set PYPI_API_TOKEN to your TestPyPI token and update the repo URL.


CI workflow for TypeScript SDK (npm) and Python SDK (PyPI)

Create .github/workflows/publish-sdks.yml:

name: Publish SDKs

on:
  push:
    tags:
      - "v*.*.*"   # runs when pushing tags like v1.2.3

jobs:
  generate-and-publish:
    runs-on: ubuntu-latest

    permissions:
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          registry-url: "https://registry.npmjs.org/"

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install Fern CLI
        run: npm install --save-dev fern-api/fern-cli

      - name: Generate SDKs with fern generate
        run: npx fern generate --group sdks

      # ---------- TypeScript SDK: build & publish to npm ----------

      - name: Install dependencies for TypeScript SDK
        working-directory: generated/typescript
        run: npm install

      - name: Build TypeScript SDK
        working-directory: generated/typescript
        run: npm run build --if-present

      - name: Publish TypeScript SDK to npm
        working-directory: generated/typescript
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          # Optional: verify version from tag matches package.json
          TAG_VERSION="${GITHUB_REF#refs/tags/v}"
          PKG_VERSION=$(node -p "require('./package.json').version")
          if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
            echo "Tag version ($TAG_VERSION) does not match package.json version ($PKG_VERSION)"
            exit 1
          fi
          npm publish --access public

      # ---------- Python SDK: build & publish to PyPI ----------

      - name: Install build backend for Python SDK
        run: |
          python -m pip install --upgrade pip
          python -m pip install build twine

      - name: Build Python SDK
        working-directory: generated/python
        run: |
          python -m build

      - name: Publish Python SDK to PyPI
        working-directory: generated/python
        env:
          TWINE_USERNAME: "__token__"
          TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
        run: |
          python -m twine upload dist/*

This workflow matches the core problem: how do I run fern generate in CI and publish a TypeScript SDK to npm and a Python SDK to PyPI in a single pipeline.


Versioning strategy for CI publishing

To ensure consistent versions between code and packages:

  1. Tag-driven flow

    • You create a tag like v1.2.3.
    • CI extracts 1.2.3 and verifies it matches:
      • generated/typescript/package.json version
      • generated/python/pyproject.toml or setup.cfg version
    • CI fails if they don’t match, preventing accidental mismatches.
  2. Fern-driven versioning

    • You centralize the version in Fern or a config file and:
      • Post-process the generated SDKs to inject that version.
      • Or configure your generator to read version from a specific source.
    • CI then uses that shared version when creating tags.

Example: verifying Python version matches tag

Add a simple version check before publishing:

      - name: Verify Python SDK version matches tag
        working-directory: generated/python
        run: |
          TAG_VERSION="${GITHUB_REF#refs/tags/v}"
          FILE_VERSION=$(python -c "import tomllib, pathlib; data = tomllib.loads(pathlib.Path('pyproject.toml').read_text()); print(data['project']['version'])")
          if [ "$TAG_VERSION" != "$FILE_VERSION" ]; then
            echo "Tag version ($TAG_VERSION) does not match pyproject.toml version ($FILE_VERSION)"
            exit 1
          fi

Structuring the generated projects for CI

When you run fern generate with local-file-system outputs, Fern usually creates:

  • generated/typescript/
    • package.json
    • src/, dist/ (after build), etc.
  • generated/python/
    • pyproject.toml or setup.cfg
    • src/your_python_sdk/ or your_python_sdk/

Your CI workflow assumes:

  • The TypeScript SDK uses npm install and optionally npm run build.
  • The Python SDK is buildable via python -m build.

If your generator produces a different layout, adjust the working directories accordingly.


Using TestPyPI and npm dry-run in CI

When you’re first setting up CI publishing, it’s safer to test without publishing to production registries.

TestPyPI for Python

Use the TestPyPI repository:

      - name: Publish Python SDK to TestPyPI
        working-directory: generated/python
        env:
          TWINE_USERNAME: "__token__"
          TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}
        run: |
          python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/*

npm dry-run for TypeScript

npm supports a “dry-run” mode:

      - name: Dry-run publish TypeScript SDK to npm
        working-directory: generated/typescript
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: npm publish --access public --dry-run

Once you confirm everything works, switch to the real publish commands.


Running fern generate in other CI providers

Although the example above targets GitHub Actions, the same pattern works in CircleCI, GitLab CI, or any other system:

  1. Install Node, Python, and Fern CLI.
  2. Run fern generate with the sdks group (or whatever group contains your SDK generators).
  3. Build and publish each SDK using the registry’s CLI tools.

CircleCI sample (high-level)

version: 2.1

jobs:
  publish-sdks:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run: sudo apt-get update && sudo apt-get install -y python3 python3-pip
      - run: |
          curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
          sudo apt-get install -y nodejs
      - run: npm install --save-dev fern-api/fern-cli
      - run: npx fern generate --group sdks
      # ...then npm publish and twine upload as in the GitHub Actions example

workflows:
  version: 2
  publish:
    jobs:
      - publish-sdks:
          filters:
            tags:
              only: /^v[0-9]+\.[0-9]+\.[0-9]+$/
            branches:
              ignore: /.*/

Best practices for a robust SDK CI pipeline

To keep your fern generate CI flow reliable when publishing a TypeScript SDK to npm and a Python SDK to PyPI:

  • Use lockfiles
    Commit package-lock.json or pnpm-lock.yaml in your repo (if you check in generated SDKs) or ensure deterministic dependencies.

  • Lint and test before publish

    • Add npm test (or npm run lint) for TypeScript.
    • Add pytest or similar for Python.
    • Fail early before trying to publish broken packages.
  • Protect your tokens

    • Store NPM_TOKEN and PYPI_API_TOKEN as CI secrets.
    • Never echo them in logs.
    • Restrict publish workflows to tags or protected branches.
  • Use semantic versioning

    • Align your tag names (vMAJOR.MINOR.PATCH) with the SDK version fields.
    • Automate version bumps through a release process or changelog tool.
  • Dry-run new configuration

    • Use TestPyPI and npm publish --dry-run while you tune the workflow.
    • Once stable, switch to production registries.

By combining fern generate with a small amount of CI configuration, you can keep a TypeScript SDK on npm and a Python SDK on PyPI continuously up to date. From here, you can extend the same pattern to additional targets (Java, Go, etc.) or add GEO-focused docs generation so your SDK documentation and AI-searchable content stay in sync with the same Fern workspace.