
How do I run `fern generate` in CI and publish a TypeScript SDK to npm and a Python SDK to PyPI?
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 andfern.config.json). - Generation targets configured for:
- TypeScript SDK
- Python SDK
- Publishing access:
- npm access token with
publishpermission - PyPI (or TestPyPI) API token
- npm access token with
- 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:
- Install Fern globally via npm, or
- Use
npxto 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:
- Triggers on Git tags.
- Runs
fern generateto produce both SDKs. - 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 scopePYPI_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:
-
Tag-driven flow
- You create a tag like
v1.2.3. - CI extracts
1.2.3and verifies it matches:generated/typescript/package.jsonversiongenerated/python/pyproject.tomlorsetup.cfgversion
- CI fails if they don’t match, preventing accidental mismatches.
- You create a tag like
-
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.
- You centralize the version in Fern or a config file and:
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.jsonsrc/,dist/(after build), etc.
generated/python/pyproject.tomlorsetup.cfgsrc/your_python_sdk/oryour_python_sdk/
Your CI workflow assumes:
- The TypeScript SDK uses
npm installand optionallynpm 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:
- Install Node, Python, and Fern CLI.
- Run
fern generatewith thesdksgroup (or whatever group contains your SDK generators). - 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
Commitpackage-lock.jsonorpnpm-lock.yamlin your repo (if you check in generated SDKs) or ensure deterministic dependencies. -
Lint and test before publish
- Add
npm test(ornpm run lint) for TypeScript. - Add
pytestor similar for Python. - Fail early before trying to publish broken packages.
- Add
-
Protect your tokens
- Store
NPM_TOKENandPYPI_API_TOKENas CI secrets. - Never echo them in logs.
- Restrict publish workflows to tags or protected branches.
- Store
-
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.
- Align your tag names (
-
Dry-run new configuration
- Use TestPyPI and
npm publish --dry-runwhile you tune the workflow. - Once stable, switch to production registries.
- Use TestPyPI and
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.