Skip to content

fix(cli): prefer x-fern-server-name over description in multi-api environment grouping - #17314

Open
iamnamananand996 wants to merge 1 commit into
mainfrom
devin/1785516784-multi-api-env-server-description
Open

fix(cli): prefer x-fern-server-name over description in multi-api environment grouping#17314
iamnamananand996 wants to merge 1 commit into
mainfrom
devin/1785516784-multi-api-env-server-description

Conversation

@iamnamananand996

@iamnamananand996 iamnamananand996 commented Jul 31, 2026

Copy link
Copy Markdown
Member

Description

Fixes Twilio pilot Issue #16: with group-multi-api-environments: true, the CLI fell back to a single-URL environment enum (using only the last spec's oauth URLs) instead of a multi-URL environment, breaking all API calls made via environment:.

Root cause: environment-name matching during multi-spec IR merging used description before the explicit server name:

// parse.ts getRawEnvironmentName
- String(server.description || server.name || server["x-fern-server-name"] || "default")
+ String(server.name || server["x-fern-server-name"] || server.description || "default")

Twilio's merged specs carry an informational servers[0].description (e.g. "Twilio Communications Unified API"). Since spec overrides merge arrays element-wise, the override's x-fern-server-name: Production landed on the same server object as the spec's description — and the description won, so environment names no longer matched across specs, detectMultipleBaseUrls returned false, and grouping never happened. (It was unrelated to the inlined securitySchemes suspected in the report; only the spec descriptions matter.)

Changes Made

  • Prefer the explicit server name (x-fern-server-name) over description when matching environments across APIs in openapi-ir-parser/src/parse.ts
  • Added regression fixture multi-api-environment-grouping-server-description (two specs sharing named environments, one server with an unrelated description) + snapshots
  • CLI changelog entry
  • Updated README.md generator (if applicable) — N/A

Testing

  • Unit tests added/updated — openapi-ir-to-fern-tests full suite passes (307 tests), new fixture snapshots produce multipleBaseUrls
  • Manual testing completed — ran fern ir on Twilio's issue-16 repro workspace; IR now emits multipleBaseUrls with twilio/oauth URLs per environment (Production/Staging/Development), matching the expected output in their report

Link to Devin session: https://app.devin.ai/sessions/7b10237605394e69a949f45a87bf31c5
Requested by: @iamnamananand996


Open in Devin Review

@iamnamananand996 iamnamananand996 self-assigned this Jul 31, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review Summary

Single-line precedence fix in getRawEnvironmentName plus a regression fixture and snapshots. The fix is correct for the reported case, but it silently changes environment names for any existing spec that has both a description and a name/x-fern-server-name on a server — that's a user-visible rename in generated SDKs worth calling out. Fixture coverage could also include the description-only fallback path.

  • 🟡 1 warning(s)
  • 🔵 2 suggestion(s)


function getRawEnvironmentName(server: SingleServerInput): string {
return String(server.description || server.name || server["x-fern-server-name"] || "default").trim();
return String(server.name || server["x-fern-server-name"] || server.description || "default").trim();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 warning

This flips precedence for all specs, not just broken ones: any workspace where a server has both a description and a name/x-fern-server-name will now get a different environment key (e.g. CoreUnifiedApiProduction), which is a rename in the generated SDK's environment enum. That's the desired outcome here, but it's technically breaking for existing users relying on the description-derived name. Consider calling this out explicitly in the changelog entry ("environment names may change if...") so downstream consumers aren't surprised by a fix-level bump.


function getRawEnvironmentName(server: SingleServerInput): string {
return String(server.description || server.name || server["x-fern-server-name"] || "default").trim();
return String(server.name || server["x-fern-server-name"] || server.description || "default").trim();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 suggestion

Worth verifying that the other environment-naming code paths (the per-spec server name resolution in the OpenAPI parser / openapi-ir-to-fern env generation) use the same precedence. If any of them still prefer description, merged env names and per-spec env names can diverge again in a different combination of fields — the same class of bug this PR fixes.

- url: https://api.stage.example.com
x-fern-server-name: Staging
- url: https://api.dev.example.com
x-fern-server-name: Development

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 suggestion

The fixture only exercises the new winner (x-fern-server-name beating description). The description-only fallback (servers with no name and no x-fern-server-name, matching across specs by description) is now the last branch and untested here — a second small fixture would lock that behavior in so a future reorder of this chain doesn't silently break description-based grouping.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@devin-ai-integration

Copy link
Copy Markdown
Contributor

✅ Verified end-to-end against Twilio's Issue #16 repro workspace

Tested by regenerating the Twilio unified SDK (8 specs, group-multi-api-environments: true) with this branch's dev CLI.

Generated TypeScript SDK environments.ts (fixed CLI, via fern generate --local with fernapi/fern-typescript-node-sdk:3.84.0):

export interface TwilioApiEnvironmentUrls {
    twilio: string;
    oauth: string;
}
export const TwilioApiEnvironment = {
    Production: { twilio: "https://api.twilio.com", oauth: "https://oauth.twilio.com" },
    Staging: { twilio: "https://api.stage-us1.twilio.com", oauth: "https://oauth.stage.twilio.com" },
    Development: { twilio: "https://api.dev.twilio.com", oauth: "https://oauth.dev.twilio.com" },
} as const;

Exactly matches the "Expected" block in the Twilio issue report; oauth.us1.twilio.com (the broken single-URL enum value) appears nowhere in the generated SDK.

Baseline: bug reproduced with published CLI (fern-api@latest)

fern ir --api latest emits:

type: singleBaseUrl
Production: https://oauth.us1.twilio.com  (urlTemplate https://oauth.{region}.twilio.com)
Staging:    https://oauth.stage.us1.twilio.com

i.e. a single-URL enum containing only the oauth spec's URLs — the two specs with an informational servers[0].description broke cross-spec environment matching.

Fixed CLI IR check
type: multipleBaseUrls, baseUrls: [twilio, oauth]
Production  {twilio: https://api.twilio.com,          oauth: https://oauth.twilio.com}
Staging     {twilio: https://api.stage-us1.twilio.com, oauth: https://oauth.stage.twilio.com}
Development {twilio: https://api.dev.twilio.com,       oauth: https://oauth.dev.twilio.com}

@github-actions

Copy link
Copy Markdown
Contributor

Docs Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-07-31T05:08:23Z).

Fixture main PR Delta
docs 256.7s (n=5) 225.1s (35 versions) -31.6s (-12.3%)

Docs generation runs fern generate --docs --preview end-to-end against the benchmark fixture with 35 API versions (each version: markdown processing + OpenAPI-to-IR + FDR upload).
Delta is computed against the nightly baseline on main.
Baseline from nightly run(s) on main (latest: 2026-07-31T05:08:23Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-07-31 17:18 UTC

@github-actions

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-07-31T05:08:23Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 80s (n=5) N/A 55s -25s (-31.2%)
go-sdk square 143s (n=5) 308s (n=5) 131s -12s (-8.4%)
java-sdk square 225s (n=5) 287s (n=5) 171s -54s (-24.0%)
php-sdk square 78s (n=5) N/A 54s -24s (-30.8%)
python-sdk square 143s (n=5) 259s (n=5) 117s -26s (-18.2%)
ruby-sdk-v2 square 109s (n=5) 151s (n=5) 84s -25s (-22.9%)
rust-sdk square 213s (n=5) 215s (n=5) 164s -49s (-23.0%)
swift-sdk square 78s (n=5) 457s (n=5) 55s -23s (-29.5%)
ts-sdk square 178s (n=5) 188s (n=5) 136s -42s (-23.6%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-07-31T05:08:23Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-07-31 17:19 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant