Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3c4ae55
fix: reuse existing PR in updateFromSourceSpec instead of creating du…
devin-ai-integration[bot] Mar 2, 2026
9ec7e45
test: add tests for updateFromSourceSpec PR reuse behavior
devin-ai-integration[bot] Mar 2, 2026
37823c8
ci: add CI workflow for tests and build verification
devin-ai-integration[bot] Mar 2, 2026
ee726d4
refactor: migrate tests from Jest to Vitest
devin-ai-integration[bot] Mar 2, 2026
5f76e07
ci: update CI workflow to use vitest
devin-ai-integration[bot] Mar 2, 2026
256055b
feat: add push fallback with rebase retry and PR comment on conflict
devin-ai-integration[bot] Mar 2, 2026
b1e6c01
fix: call setFailed when push fails with existing PR (review feedback)
devin-ai-integration[bot] Mar 2, 2026
c156b63
fix: include rebase/abort errors in PR comment instead of swallowing
devin-ai-integration[bot] Mar 2, 2026
bc361d8
feat: warn on timestamp-like branch names, include rebase abort error…
devin-ai-integration[bot] Mar 2, 2026
fc92cd5
refactor: make run() explicitly callable in tests, remove flaky setTi…
devin-ai-integration[bot] Mar 2, 2026
bb02f2f
revert: drop timestamp branch name warning (false positive risk)
devin-ai-integration[bot] Mar 2, 2026
e77f9e0
fix: use VITEST env guard instead of NODE_ENV to prevent silent break…
devin-ai-integration[bot] Mar 2, 2026
98821fd
test: add E2E test script for happy path and conflict path validation
devin-ai-integration[bot] Mar 2, 2026
4a45df4
feat: capture detailed git stderr in PR comments and default branch t…
devin-ai-integration[bot] Mar 2, 2026
0dd2dfe
fix: rename default branch to fern/sync-openapi
devin-ai-integration[bot] Mar 2, 2026
2102f86
fix: separate error paths for rebase failure vs post-rebase push failure
devin-ai-integration[bot] Mar 2, 2026
3fe89d5
fix: update stale comment on pushWithFallback
devin-ai-integration[bot] Mar 2, 2026
2106203
fix: use fenced code blocks for multi-line error messages in PR comments
devin-ai-integration[bot] Mar 2, 2026
f3c875b
ci: add release workflow to auto-update major version tag on publish
devin-ai-integration[bot] Mar 2, 2026
2f70c00
ci: also move minor version tag on release (e.g. v3.1.0 updates v3 an…
devin-ai-integration[bot] Mar 2, 2026
6cf3276
docs: update README with PR deduplication, default branch, releasing …
devin-ai-integration[bot] Mar 2, 2026
8a31f0c
docs: update README examples to reference @v4
devin-ai-integration[bot] Mar 2, 2026
223880c
chore: add biomejs, replace eslint with biome for linting and formatting
devin-ai-integration[bot] Mar 2, 2026
8ed80f7
fix: replace non-null assertions with runtime guards in syncChanges
devin-ai-integration[bot] Mar 2, 2026
996e234
chore: address PR comments - upgrade actions, vitest v4, log errors, …
devin-ai-integration[bot] Mar 2, 2026
d2bcf06
docs: add CHANGELOG.md
devin-ai-integration[bot] Mar 2, 2026
3bfab34
docs: remove [Unreleased] from changelog
devin-ai-integration[bot] Mar 2, 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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Run tests
run: npx vitest run

- name: Build
run: npm run build

- name: Check dist is up to date
run: |
git diff --exit-code dist/ || (echo "Error: dist/ is out of date. Run 'npm run build' and commit the result." && exit 1)
44 changes: 44 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Release

on:
release:
types: [published]

jobs:
tag-aliases:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Move major and minor version tags
if: ${{ !github.event.release.prerelease }}
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION="${{ github.event.release.tag_name }}"

# Only move tags if this is the latest release for the repo
LATEST=$(gh release view --repo "${{ github.repository }}" --json tagName -q .tagName)
if [ "$LATEST" != "$VERSION" ]; then
echo "Skipping: $VERSION is not the latest release (latest is $LATEST)"
exit 0
fi

STRIPPED="$(echo "$VERSION" | sed 's/^v//')"
PARTS="$(echo "$STRIPPED" | tr '.' '\n' | wc -l)"
MAJOR="v$(echo "$STRIPPED" | cut -d. -f1)"

# Always move the major tag (e.g. v3)
echo "Updating major tag: $MAJOR"
git tag -f "$MAJOR"
git push origin "refs/tags/$MAJOR" --force

# Move the minor tag only if version has 3+ parts (e.g. v3.1.0 → v3.1)
if [ "$PARTS" -ge 3 ]; then
MINOR="v$(echo "$STRIPPED" | cut -d. -f1).$(echo "$STRIPPED" | cut -d. -f2)"
echo "Updating minor tag: $MINOR"
git tag -f "$MINOR"
git push origin "refs/tags/$MINOR" --force
fi
73 changes: 73 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Changelog

All notable changes to this project will be documented in this file.

## v4

### Added

- **PR deduplication for `update_from_source` mode.** When `auto_merge` is `false`, the action now checks for an existing open PR on the branch before creating a new one. Subsequent runs push new commits to the existing PR instead of creating duplicates.
- **Push fallback with rebase retry.** If a regular push is rejected (e.g., branch has diverged), the action attempts `git pull --rebase` and retries. If rebase fails due to merge conflicts, a detailed comment is left on the PR with the full git error output and resolution steps, and the action fails.
- **Default branch name.** The `branch` input now defaults to `fern/sync-openapi`. Customers can still override it.
- **Release workflow.** Publishing a GitHub Release (e.g., `v4.1.0`) automatically force-updates the major (`v4`) and minor (`v4.1`) version tags so consumers on `@v4` or `@v4.1` stay up to date.
- **CI workflow.** Runs lint (Biome), tests (Vitest), build, and `dist/` verification on every PR and push to main.
- **E2E test script** (`e2e/run-e2e.ts`) that validates happy path (PR reuse) and conflict path (error comment) against a real test repo.
- **Biome.js** for linting and formatting, replacing ESLint. Configured with 4-space indentation, double quotes, recommended lint rules, and import sorting.
- **12 unit tests** covering PR creation, PR reuse, no-op on no changes, push without `--force`, rebase retry, PR comment on conflict, error path separation, rebase abort error handling, `setFailed` on all failure paths, and auto-merge bypass.

### Changed

- **Node runtime bumped to `node20`** (from deprecated `node16`) in `action.yml`.
- **Vitest 4** replaces Jest as the test framework.
- **CI actions updated** to `actions/checkout@v6` and `actions/setup-node@v6` with Node 20.
- **Non-null assertions replaced** with runtime guards in `syncChanges` for safer error handling.
- **Caught errors are now logged** via `core.debug()` instead of being silently ignored, aiding debugging when `ACTIONS_STEP_DEBUG` is enabled.
- **`--force` removed from `git push`** in the `updateFromSourceSpec` path so commits accumulate naturally.

### Fixed

- **Duplicate PRs.** The `updateFromSourceSpec` function previously created a new PR on every run that detected changes, even if an open PR already existed for the same branch.
- **Error messages in PR comments.** Multi-line git error output now uses fenced code blocks instead of inline code spans, fixing broken Markdown rendering on GitHub.
- **Error path separation.** `pushWithFallback` now correctly distinguishes between "rebase failed" (merge conflicts) and "rebase succeeded but push failed" (push rejection), providing accurate diagnostic labels in PR comments.

## v3

### Added

- Removed `addTimestamp` from branch names.
- Small cleanup and reformatting.

### Changed

- Updated `glob` and `js-yaml` dependencies.

## v2.1

### Fixed

- Fixed branch logic and `--force` tag handling.
- Removed date from branch names.

## v2

### Added

- Option to run `fern api upgrade`.
- Branch name formatting.
- Upstream remote support.

### Changed

- Updated actions and token handling.

## v1

### Added

- Directory and file mapping with `from`/`to` fields.
- Glob-based `exclude` patterns via `minimatch`.
- Better error messages for fetch failures.

## v0

- Initial release with basic OpenAPI spec syncing between repositories.
30 changes: 23 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A GitHub Action to [sync OpenAPI specifications with your Fern setup](/learn/api

### Case 1: Sync specs from public URL (recommended)

1. In your source repo, create a file named `sync-openapi.yml` in `.github/workflows/`.
1. In your repo, create a file named `sync-openapi.yml` in `.github/workflows/`.
2. Include the following contents in `sync-openapi.yml`:

```yaml
Expand All @@ -30,15 +30,18 @@ jobs:
with:
token: ${{ secrets.OPENAPI_SYNC_TOKEN }}
- name: Update API with Fern
uses: fern-api/sync-openapi@v2
uses: fern-api/sync-openapi@v4
with:
update_from_source: true
token: ${{ secrets.OPENAPI_SYNC_TOKEN }}
branch: 'update-api'
auto_merge: false # you MUST use auto_merge: true with branch: main

```

> **PR deduplication**: When `auto_merge` is `false`, the action creates a single PR on the `branch` (default: `fern/sync-openapi`) and accumulates commits on subsequent runs. If the source spec hasn't changed, the action is a no-op. This prevents duplicate PRs from piling up.
>
> **Branch divergence handling**: If the PR branch has diverged (e.g., someone manually rebased or edited it), the action will attempt to rebase automatically. If rebase fails due to merge conflicts, a comment is left on the PR with detailed error output and resolution steps.

### Case 2: Sync files/folders between repositories

1. In your source repo, create a file named `sync-openapi.yml` in `.github/workflows/`.
Expand All @@ -60,7 +63,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Sync OpenAPI spec to target repo
uses: fern-api/sync-openapi@v2
uses: fern-api/sync-openapi@v4
with:
repository: <your-org>/<your-target-repo>
token: ${{ secrets.<PAT_TOKEN_NAME> }}
Expand All @@ -83,12 +86,12 @@ jobs:

| Input | Description | Required | Default | Case |
|--------------------|---------------------------------------------------------------------------------------------------------------------------------------------|----------|---------------------------|---------|
| `token` | GitHub token for authentication | No | `${{ github.token }}` | 1, 2 |
| `branch` | Branch to push to in the target repository | Yes | - | 1, 2 |
| `token` | GitHub token for authentication | Yes | - | 1, 2 |
| `branch` | Branch name to create or update. **Must be a stable name** (e.g., `fern/sync-openapi`) — do not use dynamic/timestamped names, or PR deduplication will not work. | No | `fern/sync-openapi` | 1, 2 |
| `auto_merge` | If `true`, pushes directly to the branch; if `false`, creates a PR from the branch onto `main` | No | `false` | 1, 2 |
| `sources` | Array of mappings with `from`, `to`, and optional `exclude` fields | Yes | - | 2 |
| `repository` | Target repository in format `org/repo` | Yes | - | 2 |
| `update_from_source`| If `true`, syncs from the source spec files rather than using existing intermediate formats | No | `false` | 1 |
| `update_from_source`| If `true`, runs `fern api update` on the current repository instead of syncing files between repos | No | `false` | 1 |


**Note: you must set `auto_merge: true` when using `branch: main`**
Expand All @@ -107,3 +110,16 @@ The GitHub token used for this action must have:
3. Name your token (i.e. `OPENAPI_SYNC_TOKEN`) and paste in the PAT token generated above
4. Replace `<PAT_TOKEN_NAME>` in the example YAML configuration with your token name.

## Releasing

This project uses GitHub Releases to publish new versions. When a release is published, a workflow automatically updates the major and minor version tags so consumers stay up to date.

For example, publishing release `v4.1.0` will:
- Force-update the `v4` tag (so `@v4` users get the update)
- Force-update the `v4.1` tag (so `@v4.1` users get the update)

To release:
1. Go to [Releases → Draft a new release](../../releases/new)
2. Create a new tag (e.g., `v4.0.1`) following [semver](https://semver.org/)
3. Click **Publish release**

7 changes: 4 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ inputs:
required: true
branch:
description: 'Branch name to create or update'
required: true
required: false
default: 'fern/sync-openapi'
sources:
description: 'JSON or YAML array of mappings (from source to destination) (only used when update_from_source is false)'
required: false
Expand All @@ -22,8 +23,8 @@ inputs:
required: false
default: 'false'
runs:
using: 'node16'
using: 'node20'
main: 'dist/index.js'
branding:
icon: 'refresh-cw'
color: 'green'
color: 'green'
38 changes: 38 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.5/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": ["**", "!!**/dist"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 4
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "off"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
Loading