-
Notifications
You must be signed in to change notification settings - Fork 7
Add Fern Replay docs and migration guide #5497
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
ed7590a
8d35d23
7863582
aed299f
64c717c
ca8f399
2e882dc
0873572
bd7e450
92a6d94
f1391d8
8882b43
778d1be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,55 @@ | ||
| --- | ||
| title: Adding custom code | ||
| headline: Adding custom code (overview) | ||
| description: Extend Fern-generated SDKs with custom methods, logic, and dependencies. Use .fernignore to protect your code from being overwritten during regeneration. | ||
| description: Extend Fern-generated SDKs with custom methods, logic, and dependencies. Use Fern Replay for line-level edits and .fernignore for files you own end-to-end. | ||
| --- | ||
|
|
||
|
|
||
| Fern-generated SDKs are designed to be extended with custom logic, methods, and dependencies. If you want your SDK to do more than just make basic API calls (like combining multiple calls, processing data, adding utilities), you can add custom code that lives in harmony with the generated code. | ||
|
|
||
| You can also add custom methods by inheriting the Fern generated client and extending it, plus add any dependencies that your custom methods depend on in your `generators.yml` file. | ||
|
|
||
| Fern provides two complementary tools for keeping your customizations safe across regenerations: **Replay** for line-level edits to generated files, and **`.fernignore`** for files you own end-to-end. | ||
|
|
||
| ## Preserving customizations with Replay | ||
|
|
||
| <Note title="Early Access"> | ||
| Fern Replay is rolling out across customer orgs. [Contact us](https://buildwithfern.com/contact) to enable it for your organization. | ||
| </Note> | ||
|
|
||
| Replay automatically preserves the edits you make to your generated SDK across regenerations. When you commit a change to generated code, Replay records it as a patch. On the next `fern generate`, the patch is reapplied to the freshly generated SDK using a 3-way merge. | ||
|
|
||
| Replay handles the common case where `.fernignore` is too heavy-handed: you want to change a few lines, not own the whole file forever. | ||
|
|
||
| ### How it works | ||
|
|
||
| 1. Edit generated SDK code and commit to your SDK repo. | ||
| 2. On the next `fern generate`, Replay scans your repo's history for customer commits since the last `[fern-generated]` commit and stores each one as a tracked patch in `.fern/replay.lock`. | ||
| 3. The patches are reapplied on top of the freshly generated code via 3-way merge. | ||
| 4. The result lands as a `[fern-replay]` commit on top of `[fern-generated]` in the regeneration PR. | ||
|
|
||
| ```text title="Regeneration PR" | ||
| * abc123 (HEAD -> main) [fern-replay] Apply customizations | ||
| * 789abc [fern-generated] Update SDK to spec rev 0451 | ||
| * 234bcd Add helpers on top of User type | ||
| * ... | ||
| ``` | ||
|
|
||
| If the generator and your customization changed the same lines, Replay reports the conflict in the PR body. Run `fern replay resolve` locally to walk through it. | ||
|
|
||
| ### Requirements | ||
|
|
||
| Replay requires `pull-request` output mode. If your SDK uses `release` or `push` mode, see the [migration guide](/learn/sdks/overview/replay-migration) for the switch. | ||
|
|
||
| ### Disable Replay | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 [vale] reported by reviewdog 🐶 |
||
|
|
||
| To disable Replay for a specific generator, set `replay.enabled: false` in `generators.yml`. See the [`replay` reference](/learn/sdks/reference/generators-yml#replay) for global and per-generator configuration. | ||
|
|
||
| ## Using `.fernignore` to preserve your customizations | ||
|
|
||
| Once you add files containing custom code, use `.fernignore` to protect your custom code from being overwritten when Fern regenerates your SDK. | ||
| Use `.fernignore` to protect files you own end-to-end from being overwritten during SDK regeneration. Where Replay reapplies line-level edits to generated files, `.fernignore` makes the file entirely yours. The generator won't touch it, but generator updates to that file also stop flowing. | ||
|
|
||
| Simply add your custom files to the SDK repository and list them in `.fernignore`. Fern won't override any files listed there. A `.fernignore` file is automatically created in your SDK repository when you use GitHub publishing. | ||
| Add your custom files to the SDK repository and list them in `.fernignore`. A `.fernignore` file is automatically created in your SDK repository when you use GitHub publishing. | ||
|
|
||
| <Note>`.fernignore` applies only to SDK generation. It has no effect on [Fern Docs](/learn/docs/getting-started/overview) builds.</Note> | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| --- | ||
| title: Migrating to Replay | ||
| headline: Migrating from release or push mode to pull-request mode | ||
| description: Step-by-step guide for switching SDK generation from release or push mode to pull-request mode so Fern Replay can run. | ||
| --- | ||
|
|
||
| <Note title="Early Access"> | ||
| Fern Replay is rolling out across customer orgs. [Contact us](https://buildwithfern.com/contact) to enable it for your organization. | ||
| </Note> | ||
|
|
||
| [Fern Replay](/learn/sdks/overview/custom-code#preserving-customizations-with-replay) requires `pull-request` output mode. This guide covers the migration from `release` or `push` mode in five phases. | ||
|
|
||
| ## What changes | ||
|
|
||
| - SDK updates arrive as PRs instead of direct pushes to `main`. | ||
| - Customizations to generated code survive regeneration via 3-way merge. | ||
| - Version bumps are computed from pure generator output, never contaminated by customizations. | ||
| - Publishing is decoupled from generation, so you control when to release. | ||
|
|
||
| ## Before you start | ||
|
|
||
| Make sure the [Fern GitHub App](https://github.com/apps/fern-api) is installed on your SDK repositories. [Contact us](https://buildwithfern.com/contact) to flip your organization onto the Replay-enabled pipeline. | ||
|
|
||
| ## Phase 1: Move publishing secrets to the SDK repo | ||
|
|
||
| Fern sets GitHub Actions secrets in your SDK repo on every generation. When you switch to PR mode, that write would clobber any secrets you set directly. To avoid this, move secret ownership from the config repo to the SDK repo before flipping modes. | ||
|
|
||
| For each SDK repo: | ||
|
|
||
| 1. Identify which secrets your `generators.yml` references for publishing: | ||
| - Python: `PYPI_USERNAME`, `PYPI_PASSWORD` | ||
| - TypeScript / npm: `NPM_TOKEN` | ||
| - Java / Maven: `MAVEN_USERNAME`, `MAVEN_PASSWORD`, `MAVEN_SIGNATURE_KID`, `MAVEN_SIGNATURE_PASSWORD`, `MAVEN_SIGNATURE_SECRET_KEY` | ||
| - Ruby: `RUBY_GEMS_API_KEY` | ||
| - C# / NuGet: `NUGET_API_KEY` | ||
| - Rust / Crates: `CRATES_TOKEN` | ||
| 2. Add the real publish credentials directly to the SDK repo: **Settings → Secrets and variables → Actions → New repository secret**. | ||
| 3. Remove the publish credential values from the config repo's `generators.yml`: | ||
|
|
||
| ```yaml title="generators.yml" | ||
| # BEFORE | ||
| groups: | ||
| python-sdk: | ||
| generators: | ||
| - name: fernapi/fern-python-sdk | ||
| version: "..." | ||
| github: | ||
| repository: customer/python-sdk | ||
| mode: pull-request | ||
| output: | ||
| pypi: | ||
| username: $PYPI_USERNAME | ||
| password: $PYPI_PASSWORD | ||
|
|
||
| # AFTER (remove the output/pypi block entirely) | ||
| groups: | ||
| python-sdk: | ||
| generators: | ||
| - name: fernapi/fern-python-sdk | ||
| version: "..." | ||
| github: | ||
| repository: customer/python-sdk | ||
| mode: pull-request | ||
| ``` | ||
|
|
||
| 4. Run a generation. Confirm the SDK repo's secrets weren't overwritten by checking the "Last updated" timestamp under **SDK repo → Settings → Secrets**. | ||
|
|
||
| ## Phase 2: Switch output mode | ||
|
|
||
| In `generators.yml`: | ||
|
|
||
| ```yaml title="generators.yml" {3} | ||
| github: | ||
| repository: customer/python-sdk | ||
| mode: pull-request # was: mode: release (or: mode: push) | ||
| ``` | ||
|
|
||
| If you use `version: AUTO`, no other changes are needed. Autoversioning runs as part of the generator-cli pipeline and diffs pure generator output, so customer customizations never contaminate the version diff. | ||
|
|
||
| ## Phase 3: Decouple publishing from generation (optional) | ||
|
|
||
| If you want to control publishing separately from generation, keep your publish workflow out of Fern's generation cycle: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 [vale] reported by reviewdog 🐶 |
||
|
|
||
| 1. Add the workflow to `.fernignore`: | ||
|
|
||
| ```text title=".fernignore" | ||
| .github/workflows/ci.yml | ||
| ``` | ||
|
|
||
| 2. Change the workflow trigger from push-to-main to manual or release-tagged: | ||
|
|
||
| ```yaml title=".github/workflows/ci.yml" | ||
| # Instead of: | ||
| on: | ||
| push: | ||
| branches: [main] | ||
|
|
||
| # Use: | ||
| on: | ||
| workflow_dispatch: | ||
| release: | ||
| types: [published] | ||
| ``` | ||
|
|
||
| 3. Update any secret references if you renamed credentials in Phase 1. | ||
|
|
||
| ## Phase 4: First generation with Replay | ||
|
|
||
| 1. Run `fern generate --group <group-name>` from the config repo (or trigger via your existing CI). | ||
| 2. The first run auto-creates `.fern/replay.lock`. Replay tracks customer commits to generated files from this point on. | ||
| 3. Review the resulting PR. It contains: | ||
| - `[fern-generated]`: pure generator output | ||
| - `[fern-replay]`: patches re-applied (empty on first run, since no patches exist yet) | ||
| - Updated `.fern/replay.lock` | ||
| 4. Squash-merge the PR. The next generation re-anchors correctly even after the squash. | ||
|
|
||
| ## Phase 5: Validate | ||
|
|
||
| After the first generation and merge: | ||
|
|
||
| - A PR was created (not a direct push to `main`). | ||
| - No unintended package release was triggered. | ||
| - SDK code is correct and passes CI. | ||
| - `.fern/replay.lock` exists in the SDK repo. | ||
| - `.fernignore` contains `replay.lock` (and `replay.yml`). | ||
|
|
||
| After your first real customization, verify Replay's behavior end-to-end: | ||
|
|
||
| - You edit a generated file, commit, and merge to `main`. | ||
| - The next `fern generate` detects the patch (PR body shows `patches detected: 1`). | ||
| - The customization survives regeneration via a clean 3-way merge. | ||
| - `.fern/replay.lock` shows the patch stored. | ||
|
|
||
| ## Rollback | ||
|
|
||
| Replay never modifies files destructively. Your `main` branch always has correct code, so rolling back is straightforward. | ||
|
|
||
| **Quick disable.** If you only want to stop Replay and stay in `pull-request` mode, delete `.fern/replay.lock` from the SDK repo and commit. The next `fern generate` runs without Replay; PRs land in the same shape as before, just without the `[fern-replay]` commit. | ||
|
|
||
| **Full revert to `release` or `push` mode:** | ||
|
|
||
| 1. Switch `mode` back in `generators.yml`: | ||
|
|
||
| ```yaml title="generators.yml" {3} | ||
| github: | ||
| repository: customer/python-sdk | ||
| mode: release # or mode: push | ||
| ``` | ||
|
|
||
| 2. Re-add publish credentials to the config repo (if you removed them in Phase 1). | ||
| 3. Clean up Replay state in the SDK repo: | ||
| - Delete `.fern/replay.lock` | ||
| - Delete `.fern/replay.yml` | ||
| - Delete the `fern-generation-base` tag if it exists | ||
| 4. Close any open `fern-bot` PRs from PR mode. | ||
|
|
||
| ## Escape hatches | ||
|
|
||
| - `fern generate --no-replay`: skip patch application for one generation. Generation still creates `[fern-generated]`; patches aren't applied. Useful for debugging. | ||
| - `fern replay forget <pattern>`: untrack specific patches. The next generation overwrites those files. | ||
| - `fern replay reset`: delete the lockfile entirely. Your code stays, but Replay loses all state. Run `fern replay bootstrap` to start fresh. | ||
|
|
||
| ## Known caveats | ||
|
|
||
| - **Closed-without-merge replay PRs.** If a replay PR is closed without merging, the next generation detects the abandoned state and re-anchors automatically. No manual cleanup needed. | ||
| - **Force pushes and rewritten history.** Replay handles force-pushed branches via a fallback detection path. Existing patches still apply correctly. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 [vale] reported by reviewdog 🐶
[FernStyles.Headings] 'Preserving customizations with Replay' should use sentence-style capitalization.