Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
eba22be
feat(upgrade): implement fern-upgrade GitHub Action
Apr 11, 2026
28deede
refactor(upgrade): rewrite as composite action
Apr 20, 2026
ae986f8
fix(upgrade): fix repo URLs, make upgrade failures fatal, scope git a…
devin-ai-integration[bot] Apr 25, 2026
24e1d68
fix(upgrade): correct resolve-cli action path (actions/resolve-cli)
devin-ai-integration[bot] Apr 25, 2026
0faf397
fix(upgrade): save/restore fern config across branch switch to handle…
devin-ai-integration[bot] Apr 25, 2026
aad77a8
feat(upgrade): add include-major flag (default true), link changelogs…
devin-ai-integration[bot] Apr 25, 2026
4c3800f
refactor(upgrade): simplify PR title, keep version details in body
devin-ai-integration[bot] Apr 25, 2026
d4319c9
fix(upgrade): remove broken version-specific changelog anchors
devin-ai-integration[bot] Apr 25, 2026
42f2a95
test(upgrade): add unit tests for diff.js helpers (node:test, zero deps)
devin-ai-integration[bot] Apr 25, 2026
56224d0
style(upgrade): fix biome formatting in diff.test.js
devin-ai-integration[bot] Apr 25, 2026
603c771
security(upgrade): mask fern-token and github-token before CLI runs
devin-ai-integration[bot] Apr 25, 2026
07d9871
fix(upgrade): address Devin Review findings
devin-ai-integration[bot] Apr 25, 2026
cd4cb90
style(upgrade): fix biome formatting in snapshot.js
devin-ai-integration[bot] Apr 25, 2026
253cc18
security(upgrade): fix script injection — pass include-major via env var
devin-ai-integration[bot] Apr 25, 2026
843e540
refactor(upgrade): derive changelog URLs from generator name
devin-ai-integration[bot] Apr 25, 2026
ab343e8
refactor: consume fern automations upgrade CLI JSON output
devin-ai-integration[bot] Apr 29, 2026
89ff193
fix(upgrade): add CLI version check, graceful error handling, update …
devin-ai-integration[bot] Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ fern-api/actions/
│ ├── sync-openapi/ # Node.js action — TypeScript, built to dist/
│ ├── preview/ # Hybrid composite+Node.js — ALPHA
│ ├── generate/ # Node.js action — ALPHA
│ ├── upgrade/ # Node.js action — ALPHA
│ ├── upgrade/ # Composite action with JS scripts — ALPHA
│ ├── verify/ # Node.js action — ALPHA
│ └── example-action/ # Template — not released
├── packages/
Expand Down
82 changes: 82 additions & 0 deletions actions/upgrade/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# upgrade

**[ALPHA]** Automatically upgrades Fern CLI and generator versions and opens or updates a pull request with the changes.

## Requirements

- **Fern CLI version**: This action requires a CLI version that includes the `fern automations upgrade` command. Use `version: 'latest'` (the default) to ensure compatibility, or pin to a version that includes this command.

## How it works

1. Resolves and installs the Fern CLI based on the `version` input
2. Verifies the CLI supports `fern automations upgrade`
3. Runs `fern automations upgrade --json [--include-major]` — a single CLI call that:
- Upgrades the CLI version in `fern.config.json`
- Upgrades generator versions in `generators.yml`
- Returns structured JSON with all version changes
4. Formats a PR title, body, and commit message from the CLI JSON output
5. If changes were detected, pushes to the `fern/upgrade` branch and creates or updates a PR

The action uses a **single open PR model** — if a PR already exists on the `fern/upgrade` branch, it updates the PR title and body. Each run resets the branch to the latest default branch HEAD (clean slate).

## Usage

```yaml
name: Fern Upgrade
on:
schedule:
- cron: '0 6 * * *' # Daily at 6am UTC
workflow_dispatch: {} # Manual trigger + registry webhook support

permissions:
contents: write
pull-requests: write

concurrency:
group: fern-upgrade
cancel-in-progress: false

jobs:
upgrade:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: fern-api/actions/upgrade@v1
with:
fern-token: ${{ secrets.FERN_TOKEN }}
```

> **Important:** The `concurrency` block is strongly recommended to prevent race conditions if multiple upgrade runs are triggered simultaneously.

## Inputs

| Input | Required | Default | Description |
|-------|----------|---------|-------------|
| `version` | No | `latest` | Fern CLI version to use. `auto` respects `fern.config.json`, `latest` always uses newest, `inherit` uses CLI from PATH, or pin a specific version. |
| `fern-token` | **Yes** | — | Fern authentication token |
| `include-major` | No | `true` | Whether to include major version upgrades for generators. Set to `false` for minor/patch only. **Note:** This defaults to `true` (unlike the CLI default of `false`) because the upgrade action is designed to aggressively upgrade, letting the preview action validate breaking changes. |
| `github-token` | No | `${{ github.token }}` | GitHub token for PR creation and push access |

## Outputs

| Output | Description |
|--------|-------------|
| `run-id` | UUIDv4 for this upgrade run |
| `pr-url` | URL of the created or updated PR (empty if no changes) |
| `cli-upgraded` | `true` or `false` — whether the CLI version was bumped |
| `generators-upgraded` | JSON array of `{generator, from, to}` for each upgraded generator |

## PR format

**Title:** `chore(fern): upgrade CLI 0.25.0 → 0.30.0 and 3 generators`

**Body** includes:
- CLI version change
- Generator version table with changelog links
- Links to generator changelogs on [buildwithfern.com](https://buildwithfern.com)

## Permissions

The workflow requires:
- `contents: write` — to push the `fern/upgrade` branch
- `pull-requests: write` — to create and update PRs
161 changes: 157 additions & 4 deletions actions/upgrade/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,172 @@ inputs:
required: false
default: "latest"
fern-token:
description: "Fern token for API access and PR creation"
description: "Fern token for API access and CLI authentication"
required: true
include-major:
description: "Whether to include major version upgrades for generators. When true (default), major version bumps are included. Set to 'false' for minor/patch only."
required: false
default: "true"
github-token:
description: "GitHub token for PR creation and push access. Falls back to default GITHUB_TOKEN."
required: false
default: ${{ github.token }}

outputs:
run-id:
description: "UUIDv4 for this upgrade run"
value: ${{ steps.run-id.outputs.run-id }}
pr-url:
description: "URL of the created or updated upgrade PR (empty if no changes)"
value: ${{ steps.manage-pr.outputs.pr-url }}
cli-upgraded:
description: "Whether the Fern CLI version was upgraded"
description: "Whether the Fern CLI version was upgraded (true/false)"
value: ${{ steps.diff.outputs.cli-upgraded }}
generators-upgraded:
description: "JSON array of {generator, from, to} for each upgraded generator"
value: ${{ steps.diff.outputs.generators-upgraded }}

runs:
using: "node20"
main: "dist/index.js"
using: composite
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
steps:
- name: Mask secrets
shell: bash
run: |
echo "::add-mask::${{ inputs.fern-token }}"
echo "::add-mask::${{ inputs.github-token }}"

- name: Resolve Fern CLI
id: cli
uses: fern-api/actions/actions/resolve-cli@main
with:
version: ${{ inputs.version }}

- name: Set run ID
id: run-id
shell: bash
run: |
if [ -z "${FERN_RUN_ID}" ]; then
FERN_RUN_ID=$(node -e "console.log(require('crypto').randomUUID())")
echo "FERN_RUN_ID=${FERN_RUN_ID}" >> "$GITHUB_ENV"
echo "Generated new FERN_RUN_ID: ${FERN_RUN_ID}"
else
echo "Inheriting existing FERN_RUN_ID: ${FERN_RUN_ID}"
fi
echo "run-id=${FERN_RUN_ID}" >> "$GITHUB_OUTPUT"

- name: Verify CLI supports automations upgrade
shell: bash
run: |
# fern automations upgrade requires CLI version that includes this command.
# If the resolved CLI is too old, fail early with a clear message.
if ! ${{ steps.cli.outputs.fern-cmd }} automations upgrade --help >/dev/null 2>&1; then
echo "::error::The resolved Fern CLI does not support 'fern automations upgrade'. Please upgrade to the latest CLI version (set version: 'latest') or pin to a version that includes this command."
exit 1
fi

- name: Run fern automations upgrade
id: upgrade
shell: bash
env:
FERN_TOKEN: ${{ inputs.fern-token }}
INCLUDE_MAJOR: ${{ inputs.include-major }}
run: |
echo "Running fern automations upgrade..."
UPGRADE_FLAGS="--json"
if [ "$INCLUDE_MAJOR" = "true" ]; then
UPGRADE_FLAGS="$UPGRADE_FLAGS --include-major"
fi
# Capture structured JSON from stdout; logs go to stderr
if ! UPGRADE_JSON=$(${{ steps.cli.outputs.fern-cmd }} automations upgrade $UPGRADE_FLAGS); then
echo "::error::fern automations upgrade failed. Check the CLI output above for details."
exit 1
fi
if [ -z "$UPGRADE_JSON" ]; then
echo "::error::fern automations upgrade produced no output. Expected JSON on stdout."
exit 1
fi
echo "UPGRADE_JSON<<UPGRADEJSONEOF" >> "$GITHUB_ENV"
echo "$UPGRADE_JSON" >> "$GITHUB_ENV"
echo "UPGRADEJSONEOF" >> "$GITHUB_ENV"
# TODO: FER-9669 — Check automation config before running upgrades

- name: Format PR metadata
id: diff
shell: bash
env:
UPGRADE_JSON: ${{ env.UPGRADE_JSON }}
run: node "${{ github.action_path }}/scripts/diff.js"

- name: Push changes and manage PR
id: manage-pr
if: steps.diff.outputs.has-changes == 'true'
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
GITHUB_TOKEN: ${{ inputs.github-token }}
PR_TITLE: ${{ steps.diff.outputs.pr-title }}
PR_BODY: ${{ steps.diff.outputs.pr-body }}
COMMIT_MSG: ${{ steps.diff.outputs.commit-msg }}
run: |
BRANCH="fern/upgrade"

# Configure git
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

# Get default branch
DEFAULT_BRANCH=$(gh api "repos/${{ github.repository }}" --jq '.default_branch')
echo "Default branch: $DEFAULT_BRANCH"

# Warn if overwriting existing branch
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
echo "::warning::Existing $BRANCH branch will be overwritten (clean-slate strategy)"
fi

# Save upgraded fern config to temp before switching branches.
# This avoids "local changes would be overwritten" errors when the
# current checkout (e.g. a PR merge commit) differs from the default branch.
UPGRADE_TMP=$(mktemp -d)
cp -r fern/ "$UPGRADE_TMP/fern"

# Clean-slate branch from default branch HEAD
git fetch origin "$DEFAULT_BRANCH"
git checkout -f -B "$BRANCH" "origin/$DEFAULT_BRANCH"

# Restore upgraded fern config on top of the clean branch
cp -r "$UPGRADE_TMP/fern/"* fern/
rm -rf "$UPGRADE_TMP"

# Stage only fern config changes (fern.config.json + generators.yml)
git add fern/
if git diff --cached --quiet; then
echo "No changes to commit"
echo "pr-url=" >> "$GITHUB_OUTPUT"
exit 0
fi

# Commit and force push
git commit -m "$COMMIT_MSG"
git push --force origin "$BRANCH"

# Write PR body to temp file via env var to avoid shell escaping issues
printf '%s' "$PR_BODY" > /tmp/pr-body.md

# Create or update PR
EXISTING_PR=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number // empty')
if [ -n "$EXISTING_PR" ]; then
gh pr edit "$EXISTING_PR" --title "$PR_TITLE" --body-file /tmp/pr-body.md
PR_URL=$(gh pr view "$EXISTING_PR" --json url --jq '.url')
echo "Updated existing PR: $PR_URL"
else
PR_URL=$(gh pr create --head "$BRANCH" --base "$DEFAULT_BRANCH" \
--title "$PR_TITLE" --body-file /tmp/pr-body.md)
echo "Created new PR: $PR_URL"
fi

echo "pr-url=$PR_URL" >> "$GITHUB_OUTPUT"
# TODO: FER-9668 — Emit upgrade telemetry

branding:
icon: "refresh-cw"
color: "green"
Loading
Loading