Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
ba5b837
feat(java): bump to IR v66.1, support defaults on discriminants (#14890)
patrickthornton Apr 10, 2026
b9a86a1
feat(cli): route fern sdk preview through remote generation, add --lo…
devin-ai-integration[bot] Apr 10, 2026
380338b
feat(cli): refactor versioning and release procedure (#12149)
aditya-arolkar-swe Apr 10, 2026
eb1ed80
chore(java): update java-sdk seed (#14894)
fern-support Apr 10, 2026
219a0da
chore(java): update java-sdk seed (#14895)
fern-support Apr 10, 2026
f5f83c2
chore(generator-cli): upgrade @fern-api/replay to 0.10.3 (pinned), bu…
tstanmay13 Apr 10, 2026
952cbd2
chore(cli): rename FernCliError to TaskAbortSignal (#14746)
FedeZara Apr 10, 2026
d00b1f5
fix(cli): fall back to .fern/metadata.json then git tags for auto-ver…
Swimburger Apr 10, 2026
320f126
fix(python): deduplicate stream condition properties in discriminated…
devin-ai-integration[bot] Apr 10, 2026
977810b
chore(cli): make instrumentPostHogEvent synchronous (#14747)
FedeZara Apr 10, 2026
c034434
chore(rust): update rust-sdk seed (#14909)
fern-support Apr 10, 2026
e9e14f2
chore(go): update go-sdk seed (#14903)
fern-support Apr 10, 2026
6f7f6d5
chore(cli): eagerly resolve TelemetryClient distinctId via async fact…
FedeZara Apr 10, 2026
2ea9f74
chore(openapi): update openapi seed (#14905)
fern-support Apr 10, 2026
19dc7b6
chore(ruby): update ruby-sdk-v2 seed (#14904)
fern-support Apr 10, 2026
544297e
chore(go): update go-model seed (#14907)
fern-support Apr 10, 2026
798c49c
chore(python): update pydantic seed (#14902)
fern-support Apr 10, 2026
66f2d84
chore(csharp): update csharp-model seed (#14908)
fern-support Apr 10, 2026
c364997
chore(swift): update swift-sdk seed (#14906)
fern-support Apr 10, 2026
2b77a21
chore(typescript): update ts-sdk seed (#14913)
fern-support Apr 10, 2026
a006716
chore(rust): update rust-model seed (#14912)
fern-support Apr 10, 2026
62f23ba
chore(php): update php-model seed (#14910)
fern-support Apr 10, 2026
bad07c6
chore(java): update java-sdk seed (#14914)
fern-support Apr 10, 2026
0a47ef3
chore(csharp): update csharp-sdk seed (#14911)
fern-support Apr 10, 2026
86a3d03
chore(php): update php-sdk seed (#14916)
fern-support Apr 10, 2026
2deefef
chore(java): update java-model seed (#14915)
fern-support Apr 10, 2026
ae09e50
chore(php): update php-sdk seed (#14923)
fern-support Apr 10, 2026
4c79374
chore(rust): update rust-model seed (#14919)
fern-support Apr 10, 2026
12cd145
chore(go): update go-sdk seed (#14917)
fern-support Apr 10, 2026
e40690a
chore(java): update java-model seed (#14920)
fern-support Apr 10, 2026
12ce762
chore(go): update go-model seed (#14922)
fern-support Apr 10, 2026
07be755
chore(swift): update swift-sdk seed (#14918)
fern-support Apr 10, 2026
736ada0
chore(java): update java-sdk seed (#14921)
fern-support Apr 10, 2026
5ed47ed
chore(rust): update rust-sdk seed (#14924)
fern-support Apr 10, 2026
9cc5d35
chore(csharp): update csharp-model seed (#14926)
fern-support Apr 10, 2026
c0e9ba5
chore(openapi): update openapi seed (#14929)
fern-support Apr 10, 2026
cb2e05d
chore(typescript): update ts-sdk seed (#14925)
fern-support Apr 10, 2026
3003fbd
chore(ruby): update ruby-sdk-v2 seed (#14928)
fern-support Apr 10, 2026
0a03508
chore(php): update php-model seed (#14930)
fern-support Apr 10, 2026
b417a73
chore(csharp): update csharp-sdk seed (#14931)
fern-support Apr 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
49 changes: 49 additions & 0 deletions .claude/release-versioning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Changelog Infrastructure

**Source of truth:** `release-config.json` (see `release-config.schema.json`). Each **`software`** entry defines:

- **`softwareDirectory`**: repo paths under this tree are **that** product’s surface area.
- **`changelogFolder`**: where changelog files for that product live.

## What you should do

When your change touches files **under** a given entry’s **`softwareDirectory`**, add or update an **unreleased** changelog file under:

```text
{changelogFolder}/unreleased/
```

Use the **`changelogFolder`** value from `release-config.json` for that software key. Do **not** hand-edit **`versions.yml`**; versioning and rolling releases are handled by **automation**, not by running local release commands.

**Automation:** `.github/workflows/release-software.yml` drives the release pipeline (including when changes land on `main` under the configured changelog paths). Your job in a PR is to leave correct **`unreleased/`** entries so that workflow can run.

## Mapping (from `release-config.json`)

If a path falls under **`softwareDirectory`**, use the matching **`changelogFolder`/unreleased** column. **If anything here disagrees with `release-config.json`, follow the file on disk.**


| Key | `softwareDirectory` | Add changelogs under |
|-----|---------------------|----------------------|
| `cli` | `packages/cli` | `packages/cli/cli/changes/unreleased/` |
| `csharp` | `generators/csharp` | `generators/csharp/sdk/changes/unreleased/` |
| `go` | `generators/go` | `generators/go/sdk/changes/unreleased/` |
| `java` | `generators/java` | `generators/java/sdk/changes/unreleased/` |
| `php` | `generators/php` | `generators/php/sdk/changes/unreleased/` |
| `python` | `generators/python` | `generators/python/sdk/changes/unreleased/` |
| `ruby` | `generators/ruby` | `generators/ruby/sdk/changes/unreleased/` |
| `ruby-v2` | `generators/ruby-v2` | `generators/ruby-v2/sdk/changes/unreleased/` |
| `rust` | `generators/rust` | `generators/rust/sdk/changes/unreleased/` |
| `swift` | `generators/swift` | `generators/swift/sdk/changes/unreleased/` |
| `typescript` | `generators/typescript` | `generators/typescript/sdk/changes/unreleased/` |

Pick the narrowest matching **`softwareDirectory`** when several could apply (e.g. generator-only work under `generators/python/...` uses the Python generator row, not CLI). If a single PR spans multiple **`softwareDirectory`** trees, add a changelog file in **each** corresponding **`changelogFolder/unreleased/`**.

## Changelog file format

1. Copy **`.template.yml`** from that product’s `changes/unreleased/` if you need a starting point.
2. Use a new filename that describes the change (e.g. `fix-streaming-timeout.yml`).
3. Each file is a **YAML array** of objects. Only these fields are allowed (see **`fern-changes-yml.schema.json`**):
- **`summary`**: string (multi-line `|` allowed).
- **`type`**: `fix` | `chore` | `feat` | `internal` | `break`

Those types drive semver bumps when releases run (`fix`/`chore` → patch, `feat`/`internal` → minor, `break` → major). You do not need to bump versions yourself.
90 changes: 90 additions & 0 deletions .github/workflows/release-software.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: Release Software

on:
push:
branches:
- main
paths:
# Auto-updated by scripts/release-setup.ts — do not edit manually
- "packages/cli/cli/changes/**"
- "generators/csharp/sdk/changes/**"
- "generators/go/sdk/changes/**"
- "generators/java/sdk/changes/**"
- "generators/php/sdk/changes/**"
- "generators/python/sdk/changes/**"
- "generators/ruby/sdk/changes/**"
- "generators/ruby-v2/sdk/changes/**"
- "generators/rust/sdk/changes/**"
- "generators/swift/sdk/changes/**"
- "generators/typescript/sdk/changes/**"
workflow_dispatch:
inputs:
software:
description: "Which software to release"
required: false
default: "all"
type: choice
options:
# Auto-updated by scripts/release-setup.ts — do not edit manually
- all
- cli
- csharp
- go
- java
- php
- python
- ruby
- ruby-v2
- rust
- swift
- typescript

# Prevent concurrent release runs to avoid git conflicts
concurrency:
group: release-software
cancel-in-progress: false

# ACTIONS_PAT must belong to a user with bypass access for the main branch
# protection rule, scoped only to allow changelog/versions file commits.
permissions:
contents: write

jobs:
release-software:
if: github.repository == 'fern-api/fern'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.ACTIONS_PAT }}
ref: main
fetch-depth: 0

- name: Skip if triggered by own release commit
id: skip-release-loop
run: |
SUBJECT=$(git log -1 --format='%s')
echo "Last commit subject: $SUBJECT"
if [[ "$SUBJECT" =~ ^chore\(.+\):\ release\ .+ ]]; then
echo "Triggered by automated release commit — skipping to avoid loop."
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi

- name: Configure git
if: steps.skip-release-loop.outputs.skip != 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Install
if: steps.skip-release-loop.outputs.skip != 'true'
uses: ./.github/actions/install

- name: Run releases for all configured software
if: steps.skip-release-loop.outputs.skip != 'true'
env:
SOFTWARE: ${{ inputs.software || 'all' }}
run: npx tsx scripts/release-workflow.ts "$SOFTWARE"
42 changes: 42 additions & 0 deletions fern-changes-yml.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Changelog Entry",
"description": "Schema for changelog entries in the unreleased directory",
"type": "array",
"items": {
"$ref": "#/definitions/ChangelogEntry"
},
"definitions": {
"ChangelogEntry": {
"type": "object",
"additionalProperties": false,
"properties": {
"summary": {
"type": "string",
"description": "Description of the change. Use multi-line format for longer descriptions."
},
"type": {
"$ref": "#/definitions/ChangelogType",
"description": "Type of change determines version bump: fix/chore (patch), feat/internal (minor), break (major)"
}
},
"required": [
"summary",
"type"
],
"title": "Changelog"
},
"ChangelogType": {
"type": "string",
"enum": [
"fix",
"chore",
"feat",
"internal",
"break"
],
"title": "Changelog Type",
"description": "The type of change: fix/chore → patch, feat/internal → minor, break → major"
}
}
}
Empty file.
8 changes: 8 additions & 0 deletions generators/csharp/sdk/changes/unreleased/.template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Template for changelog entries
# Copy this file and rename it to describe your change (e.g., add-auth-feature.yml)
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json

- summary: |
Brief description of your change.
You can use multiple lines for longer descriptions.
type: feat # Options: fix/chore (patch), feat/internal (minor), break (major)
2 changes: 2 additions & 0 deletions generators/go/sdk/changes/unreleased/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This directory contains unreleased changelog entries
# Create files here following the format in fern-changes-yml.schema.json
8 changes: 8 additions & 0 deletions generators/go/sdk/changes/unreleased/.template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Template for changelog entries
# Copy this file and rename it to describe your change (e.g., add-auth-feature.yml)
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json

- summary: |
Brief description of your change.
You can use multiple lines for longer descriptions.
type: feat # Options: fix/chore (patch), feat/internal (minor), break (major)
2 changes: 1 addition & 1 deletion generators/java-v2/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"@fern-api/fs-utils": "workspace:*",
"@fern-api/java-ast": "workspace:*",
"@fern-api/logging-execa": "workspace:*",
"@fern-fern/ir-sdk": "^66.0.0",
"@fern-fern/ir-sdk": "^66.1.0",
"@types/node": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
Expand Down
2 changes: 1 addition & 1 deletion generators/java-v2/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"@fern-api/logger": "workspace:*",
"@fern-fern/generator-cli-sdk": "^0.1.8",
"@fern-fern/generator-exec-sdk": "catalog:",
"@fern-fern/ir-sdk": "^66.0.0",
"@fern-fern/ir-sdk": "^66.1.0",
"@types/lodash-es": "catalog:",
"@types/node": "catalog:",
"lodash-es": "catalog:",
Expand Down
2 changes: 1 addition & 1 deletion generators/java/generator-utils/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
dependencies {
api 'com.squareup:javapoet'
api 'com.fern.fern:irV65'
api 'com.fern.fern:irV66'
api 'com.fern.fern:generator-exec-client'
api 'com.fasterxml.jackson.core:jackson-annotations'
api 'com.fasterxml.jackson.core:jackson-databind'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
import com.fern.generator.exec.model.logging.SuccessfulStatusUpdate;
import com.fern.ir.core.ObjectMappers;
import com.fern.ir.model.commons.Name;
import com.fern.ir.model.commons.NameAndWireValue;
import com.fern.ir.model.commons.NameAndWireValueOrString;
import com.fern.ir.model.commons.NameOrString;
import com.fern.ir.model.ir.IntermediateRepresentation;
import com.fern.ir.model.publish.DirectPublish;
import com.fern.ir.model.publish.Filesystem;
Expand Down Expand Up @@ -127,14 +128,16 @@ private static IntermediateRepresentation getIr(GeneratorConfig generatorConfig)
CasingConfiguration casingConfig = CasingConfiguration.fromIrJson(rootNode);

SimpleModule nameModule = new SimpleModule("NameOrStringModule");
nameModule.addDeserializer(Name.class, new NameDeserializer(casingConfig));
nameModule.addDeserializer(NameAndWireValue.class, new NameAndWireValueDeserializer(casingConfig));
nameModule.addDeserializer(Name.class, new NameDeserializer.RawNameDeserializer(casingConfig));
nameModule.addDeserializer(NameOrString.class, new NameDeserializer(casingConfig));
nameModule.addDeserializer(NameAndWireValueOrString.class, new NameAndWireValueDeserializer(casingConfig));

ObjectMapper irMapper = ObjectMappers.JSON_MAPPER.copy();
// Mix-in annotations override class-level @JsonDeserialize(builder=...) annotations,
// allowing the module-level custom deserializers to take effect.
irMapper.addMixIn(Name.class, NameOrStringMixIn.class);
irMapper.addMixIn(NameAndWireValue.class, NameOrStringMixIn.class);
irMapper.addMixIn(NameOrString.class, NameOrStringMixIn.class);
irMapper.addMixIn(NameAndWireValueOrString.class, NameOrStringMixIn.class);
irMapper.registerModule(nameModule);

return irMapper.treeToValue(rootNode, IntermediateRepresentation.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.fern.ir.model.commons.SafeAndUnsafeString;
import com.fern.ir.model.types.DeclaredTypeName;
import com.fern.java.utils.KeyWordUtils;
import com.fern.java.utils.NameUtils;
import com.squareup.javapoet.ClassName;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -37,14 +38,19 @@ public AbstractModelPoetClassNameFactory(
public final ClassName getTypeClassName(DeclaredTypeName declaredTypeName) {
String packageName = getTypesPackageName(declaredTypeName.getFernFilepath());
return ClassName.get(
packageName, declaredTypeName.getName().getPascalCase().getSafeName());
packageName,
NameUtils.toName(declaredTypeName.getName()).getPascalCase().getSafeName());
}

@Override
public final ClassName getInterfaceClassName(DeclaredTypeName declaredTypeName) {
String packageName = getTypesPackageName(declaredTypeName.getFernFilepath());
return ClassName.get(
packageName, "I" + declaredTypeName.getName().getPascalCase().getSafeName());
packageName,
"I"
+ NameUtils.toName(declaredTypeName.getName())
.getPascalCase()
.getSafeName());
}

protected final String getTypesPackageName(FernFilepath fernFilepath) {
Expand All @@ -54,6 +60,7 @@ protected final String getTypesPackageName(FernFilepath fernFilepath) {
// NOTE: We're making it camel-case here on purpose: snake-case is used in the NESTED case for
// historical reasons, but we should unify on this method going forward.
tokens.addAll(fernFilepath.getPackagePath().stream()
.map(NameUtils::toName)
.map(Name::getCamelCase)
.map(SafeAndUnsafeString::getSafeName)
.map(String::toLowerCase)
Expand All @@ -64,6 +71,7 @@ protected final String getTypesPackageName(FernFilepath fernFilepath) {
default:
tokens.add("model");
tokens.addAll(fernFilepath.getAllParts().stream()
.map(NameUtils::toName)
.map(Name::getSnakeCase)
.map(SafeAndUnsafeString::getSafeName)
.flatMap(snakeCase -> splitOnNonAlphaNumericChar(snakeCase).stream())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.fern.ir.model.commons.SafeAndUnsafeString;
import com.fern.ir.model.types.DeclaredTypeName;
import com.fern.java.utils.KeyWordUtils;
import com.fern.java.utils.NameUtils;
import com.squareup.javapoet.ClassName;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -41,14 +42,19 @@ public AbstractNonModelPoetClassNameFactory(
public final ClassName getTypeClassName(DeclaredTypeName declaredTypeName) {
String packageName = getTypesPackageName(declaredTypeName.getFernFilepath());
return ClassName.get(
packageName, declaredTypeName.getName().getPascalCase().getSafeName());
packageName,
NameUtils.toName(declaredTypeName.getName()).getPascalCase().getSafeName());
}

@Override
public final ClassName getInterfaceClassName(DeclaredTypeName declaredTypeName) {
String packageName = getTypesPackageName(declaredTypeName.getFernFilepath());
return ClassName.get(
packageName, "I" + declaredTypeName.getName().getPascalCase().getSafeName());
packageName,
"I"
+ NameUtils.toName(declaredTypeName.getName())
.getPascalCase()
.getSafeName());
}

protected final String getTypesPackageName(FernFilepath fernFilepath) {
Expand All @@ -64,6 +70,7 @@ protected final String getResourcesPackage(Optional<FernFilepath> fernFilepath,
switch (packageLayout) {
case FLAT:
fernFilepath.ifPresent(filePath -> tokens.addAll(filePath.getPackagePath().stream()
.map(NameUtils::toName)
.map(Name::getCamelCase)
.map(SafeAndUnsafeString::getSafeName)
.map(String::toLowerCase)
Expand All @@ -77,6 +84,7 @@ protected final String getResourcesPackage(Optional<FernFilepath> fernFilepath,
tokens.add("resources");
}
fernFilepath.ifPresent(filepath -> tokens.addAll(filepath.getAllParts().stream()
.map(NameUtils::toName)
.map(Name::getCamelCase)
.map(SafeAndUnsafeString::getSafeName)
// names should be lower case
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fern.ir.model.ir.IntermediateRepresentation;
import com.fern.ir.model.types.DeclaredTypeName;
import com.fern.java.utils.KeyWordUtils;
import com.fern.java.utils.NameUtils;
import com.squareup.javapoet.ClassName;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -126,7 +127,7 @@ public static List<String> getPackagePrefixWithOrgAndApiName(IntermediateReprese
prefix.add("com");
prefix.addAll(splitOnNonAlphaNumericChar(KeyWordUtils.getKeyWordCompatibleName(organization)));
prefix.addAll(splitOnNonAlphaNumericChar(KeyWordUtils.getKeyWordCompatibleName(
ir.getApiName().getCamelCase().getSafeName())));
NameUtils.toName(ir.getApiName()).getCamelCase().getSafeName())));
return prefix;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.fern.ir.model.auth.BearerAuthScheme;
import com.fern.ir.model.auth.OAuthScheme;
import com.fern.ir.model.commons.Name;
import com.fern.ir.model.commons.NameOrString;
import com.fern.ir.model.commons.SafeAndUnsafeString;
import com.fern.ir.model.ir.IntermediateRepresentation;
import java.util.List;
Expand All @@ -18,7 +19,7 @@ public class FeatureResolver {

public static final AuthScheme DEFAULT_BEARER_AUTH = AuthScheme.bearer(BearerAuthScheme.builder()
.key(AuthSchemeKey.of("bearer"))
.token(Name.builder()
.token(NameOrString.of(Name.builder()
.originalName("token")
.camelCase(SafeAndUnsafeString.builder()
.unsafeName("token")
Expand All @@ -36,7 +37,7 @@ public class FeatureResolver {
.unsafeName("TOKEN")
.safeName("TOKEN")
.build())
.build())
.build()))
.build());
private final IntermediateRepresentation ir;
private final GeneratorConfig generatorConfig;
Expand Down
Loading
Loading