> For clean Markdown content of this page, append .md to this URL. For the complete documentation index, see https://buildwithfern.com/learn/llms.txt. # Publishing > Publish your generated CLI to npm, GitHub Releases, Homebrew, and Scoop with automated cross-platform builds. #### Early access The CLI generator is in early access. [Reach out](https://buildwithfern.com/book-demo?type=cli) to get started. Publish your generated CLI to npm. After following the steps on this page, each tagged release builds cross-platform binaries, attaches them to a GitHub Release, and publishes them to npm automatically. Homebrew and Scoop are opt-in channels on top of the same release. To keep generation on your own infrastructure, you can [self-host the generator](/learn/cli-generator/get-started/local-generation) instead of using Fern's cloud. This page assumes that you have: * An initialized `fern` folder with `generators.yml` configured for the CLI generator. See [Quickstart](/learn/cli-generator/get-started/quickstart). * An existing GitHub repository for the generated CLI source, with the [Fern GitHub App](https://github.com/apps/fern-api) installed on it. ## Configure output location #### Set the output location to npm In the `group` for your CLI, set the output location to `npm`: **`generators.yml`** ```yaml title="generators.yml" {6-7} groups: cli: generators: - name: fern-cli-generator version: 0.40.0 output: location: npm config: binaryName: my-cli ``` #### Add a unique package name The package name must be unique in the npm registry. **`generators.yml`** ```yaml title="generators.yml" {10} groups: cli: generators: - name: fern-cli-generator version: 0.40.0 output: location: npm package-name: "@myorg/my-cli" config: binaryName: my-cli ``` The npm package name is independent of the generated Rust crate's identity. To publish the crate under your own name, license, repository, and authors, set [`packageIdentity`](/learn/cli-generator/get-started/configuration#config-options) in the generator's `config`. ## Configure GitHub publishing Fern publishes your CLI via GitHub Actions. Configure your GitHub repository and publishing mode: **`generators.yml`** ```yaml title="generators.yml" {11-13} groups: cli: generators: - name: fern-cli-generator version: 0.40.0 output: location: npm package-name: "@myorg/my-cli" config: binaryName: my-cli github: repository: my-org/my-cli mode: release ``` The `repository` owner is independent of your Fern organization (the `--organization` value passed to `fern init`). Point it at the GitHub user or organization that owns the target repository. | Mode | Behavior | | -------------- | -------------------------------------------------------------------------------------------------------------- | | `release` | Commits to the default branch and tags a release. The CI workflow builds binaries and publishes automatically. | | `pull-request` | Opens a PR with generated source for review. Merge the PR and create a GitHub release to trigger publishing. | ## Configure authentication Choose how to authenticate with npm when publishing. #### OIDC authentication (Recommended) OIDC-based publishing (trusted publishing) is the most secure option. npm trusts your GitHub repository to publish directly — no tokens to manage. #### Add OIDC to generators.yml Set `token: OIDC` in the `output` section: **`generators.yml`** ```yaml title="generators.yml" {9} groups: cli: generators: - name: fern-cli-generator version: 0.40.0 output: location: npm package-name: "@myorg/my-cli" token: OIDC config: binaryName: my-cli github: repository: my-org/my-cli mode: release ``` #### Create the packages on npm The CI workflow publishes one launcher package and one package per platform. For a package named `@myorg/my-cli`, that's: | Package | Contents | | ---------------------------- | -------------------------- | | `@myorg/my-cli` | Node.js launcher | | `@myorg/my-cli-linux-x64` | Linux x86\_64 binary | | `@myorg/my-cli-linux-arm64` | Linux ARM64 binary | | `@myorg/my-cli-darwin-x64` | macOS Intel binary | | `@myorg/my-cli-darwin-arm64` | macOS Apple Silicon binary | | `@myorg/my-cli-win32-x64` | Windows x86\_64 binary | OIDC can't perform a package's first publish; npm requires a package to exist before you can add a trusted publisher. Run the following script to create and publish a placeholder for each package: ```bash npm login PACKAGE_NAME="@myorg/my-cli" ACCESS="public" # or "restricted" for private packages for pkg in \ "$PACKAGE_NAME" \ "$PACKAGE_NAME-linux-x64" \ "$PACKAGE_NAME-linux-arm64" \ "$PACKAGE_NAME-darwin-x64" \ "$PACKAGE_NAME-darwin-arm64" \ "$PACKAGE_NAME-win32-x64" do dir="$(mktemp -d)" echo "{ \"name\": \"$pkg\", \"version\": \"0.0.0\" }" > "$dir/package.json" npm publish "$dir" --access "$ACCESS" done ``` Publish all the packages listed above, not only the launcher — an OIDC run fails on any that doesn't yet exist. Set the placeholder version to something below the first CI release (e.g. `0.0.0`). If the account has two-factor authentication enabled, `npm login` (a browser session) can't publish. Instead, have the user create a [granular access token](https://docs.npmjs.com/creating-and-viewing-access-tokens) with **Read and Write** access to all packages, then set it locally with `npm config set "//registry.npmjs.org/:_authToken" ` and publish. This token is only for the local bootstrap; CI uses OIDC. #### Authorize your repository on npmjs.com Configure [trusted publishing](https://docs.npmjs.com/trusted-publishers) on npmjs.com. Repeat this for **every** package listed above — an OIDC run fails on any package that has no trusted publisher. 1. Open the package page on npmjs.com and go to the **Settings** tab 2. Find the **Trusted Publisher** section and click **Add trusted publisher** 3. Select **GitHub Actions** as your provider 4. Fill in: * **Organization or user**: Your GitHub username or organization * **Repository**: Your CLI repository name (e.g. `my-cli`) * **Workflow filename**: `ci.yml` * **Environment name**: Leave blank 5. Under **Allowed actions**, select **Allow `npm publish`** #### Generate the CLI Generate the CLI to create the GitHub Actions workflow with OIDC configuration: ```bash fern generate --group cli ``` This creates a `.github/workflows/ci.yml` file configured for OIDC npm publishing. In `release` mode, it also tags a release, which triggers the first publish run over the placeholder versions. #### Token-based authentication #### Generate an npm token 1. Log into [npmjs.com](https://www.npmjs.com/) 2. Click your profile picture and select **Access Tokens** 3. Click **Generate New Token** and create a [granular access token](https://docs.npmjs.com/creating-and-viewing-access-tokens) 4. Give it **Read and Write** access to **All packages** 5. Save your token securely #### Add token to generators.yml Set `token: ${NPM_TOKEN}` in the `output` section: **`generators.yml`** ```yaml title="generators.yml" {9} groups: cli: generators: - name: fern-cli-generator version: 0.40.0 output: location: npm package-name: "@myorg/my-cli" token: ${NPM_TOKEN} config: binaryName: my-cli github: repository: my-org/my-cli mode: release ``` #### Add NPM\_TOKEN as a GitHub Actions secret 1. Open your CLI repository on GitHub and go to **Settings** 2. Navigate to **Secrets and variables** > **Actions** 3. Click **New repository secret** 4. Name it `NPM_TOKEN` and paste your npm token 5. Click **Add secret** ## Configure distribution channels Homebrew and Scoop are optional channels that install from the archives attached to each GitHub Release, so both require [GitHub publishing](#configure-github-publishing). Add the channels you want under [`distribution`](/learn/cli-generator/get-started/configuration#config-options) in the generator's `config`: **`generators.yml`** ```yaml title="generators.yml" {10-16} groups: cli: generators: - name: fern-cli-generator version: 0.40.0 output: location: npm package-name: "@myorg/my-cli" config: binaryName: my-cli distribution: homebrew: tap: my-org/homebrew-tap formula: my-cli scoop: bucket: my-org/scoop-bucket github: repository: my-org/my-cli mode: release ``` On the next generation, Fern adds a `publish-homebrew-formula` job, a `publish-scoop` job, or both to the release workflow, along with the matching install commands in the generated README. Prereleases are skipped: a tap and a bucket have no prerelease channel, so an RC never becomes the version users install. Each channel needs a repository and credentials before that first release: #### Create the tap and bucket repositories Create a public GitHub repository for each channel you enable — conventionally `homebrew-tap` and `scoop-bucket`. They can be empty; the release workflow commits the formula and the manifest into them. A private tap or bucket breaks installation for users who lack access to it. #### Add credentials with write access to them The workflow's built-in `GITHUB_TOKEN` is scoped to the CLI repository and can't push to another one, so each channel authenticates with credentials you store as Actions secrets in the CLI repository, under **Settings** > **Secrets and variables** > **Actions**. A personal access token is the quickest to set up; a GitHub App keeps release access off any individual's account. #### Personal access token One token per channel, each with write access to that channel's repository: | Secret | Channel | Overridden by | | -------------------- | -------- | ------------------------------------------------ | | `HOMEBREW_TAP_TOKEN` | Homebrew | `distribution.homebrew.tokenEnvironmentVariable` | | `SCOOP_BUCKET_TOKEN` | Scoop | `distribution.scoop.tokenEnvironmentVariable` | A token belongs to a person and doesn't expire, so releases break once that person's access ends. #### GitHub App One App serves both channels: each publish job mints an installation token at release time, scoped to a single repository and valid for an hour, and revoking access becomes uninstalling the App. 1. In the account that owns the tap or bucket, go to **Settings** > **Developer settings** > **GitHub Apps** > **New GitHub App**. Grant the **Contents: Read and write** repository permission and clear **Webhook** > **Active**. Record the App ID, then generate a private key and download the `.pem` file. 2. From the App's page, select **Install App** and limit the installation to the tap and bucket repositories. A tap and a bucket under different accounts each need the App installed on their own account: every job requests a token for the owner of the repository it pushes to. 3. Add two Actions secrets to the CLI repository, one holding the App ID and one holding the entire contents of the `.pem` file. An Actions *variable* isn't readable as a secret, and a private key pasted as a single line still carries its `BEGIN` header while failing to sign, which surfaces as `secretOrPrivateKey must be an asymmetric key`, so keep the key's line breaks intact. 4. Reference both secret names from [`distribution.githubApp`](/learn/cli-generator/get-started/configuration#config-options): **`generators.yml`** ```yaml title="generators.yml" {3-5} config: distribution: githubApp: appIdSecret: PUBLISH_APP_ID privateKeySecret: PUBLISH_APP_PRIVATE_KEY homebrew: tap: garden/homebrew-tap scoop: bucket: garden/scoop-bucket ``` The next generation also adds a `preflight-distribution` job that fails within seconds of the release starting if either secret is missing or malformed. ## Publish your CLI How you trigger a publish depends on your `mode`: * In `release` mode, `fern generate --group cli` commits the updated source and tags a release. The tag triggers the CI workflow (no manual release step). * In `pull-request` mode, `fern generate --group cli` opens a PR. Merge it, then create a GitHub release with a version tag (e.g. `v1.0.0`) to trigger publishing. The CI workflow then builds binaries for all platforms, attaches the archives and the `curl | bash` and PowerShell installers to the GitHub Release, publishes to npm, and updates any configured tap or bucket. ## Build targets The CI workflow produces statically linked binaries for: | Target | OS | Architecture | | --------------------------- | ------- | ------------- | | `x86_64-unknown-linux-gnu` | Linux | x86\_64 | | `aarch64-unknown-linux-gnu` | Linux | ARM64 | | `x86_64-apple-darwin` | macOS | Intel | | `aarch64-apple-darwin` | macOS | Apple Silicon | | `x86_64-pc-windows-msvc` | Windows | x86\_64 | The npm package wraps the native binary with a Node.js launcher. Platform-specific optional dependencies ensure only the correct binary downloads at install time. ## Install instructions for users After publishing, direct users to whichever channel they prefer. Every generated CLI supports npm and the installers attached to the GitHub Release: ```bash npm install -g @myorg/my-cli ``` With `distribution.homebrew` configured, a tap named `my-org/homebrew-tap` resolves as `my-org/tap`: ```bash brew install my-org/tap/my-cli ``` With `distribution.scoop` configured, users add the bucket once, then install: ```bash scoop bucket add my-org https://github.com/my-org/scoop-bucket scoop install my-cli ``` Scoop covers x86\_64 Windows only; ARM64 Windows users install through npm or the PowerShell installer. `self-update` replaces the binary in place rather than deferring to `brew upgrade` or `scoop update`, so those package managers can report an older version than the one installed. > Publish your generated CLI to npm, GitHub Releases, Homebrew, and Scoop with automated cross-platform builds.