From 8b09ac2c3d9849e421eed299bc2b5c5027001fbc Mon Sep 17 00:00:00 2001 From: "naman.anand@postman.com" Date: Fri, 31 Jul 2026 16:53:05 +0000 Subject: [PATCH] fix(cli): prefer x-fern-server-name over description in multi-api environment grouping --- .../openapi/openapi-ir-parser/src/parse.ts | 2 +- ...vironment-grouping-server-description.json | 376 ++++++++++++++++++ ...vironment-grouping-server-description.json | 224 +++++++++++ .../auth-api.yml | 43 ++ .../core-api.yml | 35 ++ .../fern.config.json | 4 + .../fern/generators.yml | 9 + ...nvironment-grouping-server-description.yml | 6 + 8 files changed, 698 insertions(+), 1 deletion(-) create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/multi-api-environment-grouping-server-description.json create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/multi-api-environment-grouping-server-description.json create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/auth-api.yml create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/core-api.yml create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/fern.config.json create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/fern/generators.yml create mode 100644 packages/cli/cli/changes/unreleased/fix-multi-api-environment-grouping-server-description.yml diff --git a/packages/cli/api-importers/openapi/openapi-ir-parser/src/parse.ts b/packages/cli/api-importers/openapi/openapi-ir-parser/src/parse.ts index 33cdb822eb6b..75d9dcc5cdf8 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-parser/src/parse.ts +++ b/packages/cli/api-importers/openapi/openapi-ir-parser/src/parse.ts @@ -212,7 +212,7 @@ interface MultiApiEndpoint extends Endpoint { type TypedEndpoint = StandardEndpoint | MultiApiEndpoint; 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(); } function getEnvironmentName(server: SingleServerInput): string { diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/multi-api-environment-grouping-server-description.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/multi-api-environment-grouping-server-description.json new file mode 100644 index 000000000000..7af91de35927 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/multi-api-environment-grouping-server-description.json @@ -0,0 +1,376 @@ +{ + "specVersion": "1.0.0", + "title": "Core API", + "servers": [ + { + "type": "grouped", + "name": "Production", + "description": "Production environment", + "urls": { + "api": { + "url": "https://api.example.com" + }, + "auth": { + "url": "https://auth.example.com" + } + } + }, + { + "type": "grouped", + "name": "Staging", + "description": "Staging environment", + "urls": { + "api": { + "url": "https://api.stage.example.com" + }, + "auth": { + "url": "https://auth.stage.example.com" + } + } + }, + { + "type": "grouped", + "name": "Development", + "description": "Development environment", + "urls": { + "api": { + "url": "https://api.dev.example.com" + }, + "auth": { + "url": "https://auth.dev.example.com" + } + } + } + ], + "websocketServers": [], + "tags": { + "tagsById": {} + }, + "hasEndpointsMarkedInternal": false, + "endpoints": [ + { + "summary": "List widgets", + "audiences": [], + "operationId": "listWidgets", + "tags": [], + "pathParameters": [], + "queryParameters": [], + "headers": [], + "generatedRequestName": "ListWidgetsRequest", + "response": { + "description": "Successful response", + "schema": { + "value": { + "generatedName": "ListWidgetsResponseItem", + "schema": "Widget", + "source": { + "file": "../core-api.yml", + "type": "openapi" + }, + "type": "reference" + }, + "generatedName": "ListWidgetsResponse", + "groupName": [], + "type": "array" + }, + "fullExamples": [], + "source": { + "file": "../core-api.yml", + "type": "openapi" + }, + "statusCode": 200, + "type": "json" + }, + "errors": {}, + "authed": false, + "method": "GET", + "path": "/widgets", + "examples": [ + { + "pathParameters": [], + "queryParameters": [], + "headers": [], + "response": { + "value": { + "value": [ + { + "properties": { + "id": { + "value": { + "value": "id", + "type": "string" + }, + "type": "primitive" + }, + "name": { + "value": { + "value": "name", + "type": "string" + }, + "type": "primitive" + } + }, + "type": "object" + } + ], + "type": "array" + }, + "type": "withoutStreaming" + }, + "codeSamples": [], + "type": "full" + } + ], + "source": { + "file": "../core-api.yml", + "type": "openapi" + }, + "__apiName": "api", + "servers": [ + { + "name": "api" + } + ] + }, + { + "summary": "Issue a token", + "audiences": [], + "operationId": "issueToken", + "tags": [], + "pathParameters": [], + "queryParameters": [], + "headers": [], + "generatedRequestName": "IssueTokenRequest", + "request": { + "schema": { + "generatedName": "IssueTokenRequest", + "schema": "TokenRequest", + "source": { + "file": "../auth-api.yml", + "type": "openapi" + }, + "type": "reference" + }, + "contentType": "application/json", + "fullExamples": [], + "additionalProperties": false, + "source": { + "file": "../auth-api.yml", + "type": "openapi" + }, + "type": "json" + }, + "response": { + "description": "Successful response", + "schema": { + "generatedName": "IssueTokenResponse", + "schema": "TokenResponse", + "source": { + "file": "../auth-api.yml", + "type": "openapi" + }, + "type": "reference" + }, + "fullExamples": [], + "source": { + "file": "../auth-api.yml", + "type": "openapi" + }, + "statusCode": 200, + "type": "json" + }, + "errors": {}, + "authed": false, + "method": "POST", + "path": "/token", + "examples": [ + { + "pathParameters": [], + "queryParameters": [], + "headers": [], + "request": { + "properties": {}, + "type": "object" + }, + "response": { + "value": { + "properties": { + "accessToken": { + "value": { + "value": "accessToken", + "type": "string" + }, + "type": "primitive" + } + }, + "type": "object" + }, + "type": "withoutStreaming" + }, + "codeSamples": [], + "type": "full" + } + ], + "source": { + "file": "../auth-api.yml", + "type": "openapi" + }, + "__apiName": "auth", + "servers": [ + { + "name": "auth" + } + ] + } + ], + "webhooks": [], + "channels": {}, + "groupedSchemas": { + "rootSchemas": { + "Widget": { + "allOf": [], + "properties": [ + { + "conflict": {}, + "generatedName": "widgetId", + "key": "id", + "schema": { + "generatedName": "WidgetId", + "value": { + "schema": { + "type": "string" + }, + "generatedName": "WidgetId", + "groupName": [], + "type": "primitive" + }, + "groupName": [], + "type": "optional" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "widgetName", + "key": "name", + "schema": { + "generatedName": "WidgetName", + "value": { + "schema": { + "type": "string" + }, + "generatedName": "WidgetName", + "groupName": [], + "type": "primitive" + }, + "groupName": [], + "type": "optional" + }, + "audiences": [] + } + ], + "allOfPropertyConflicts": [], + "generatedName": "Widget", + "groupName": [], + "additionalProperties": false, + "source": { + "file": "../core-api.yml", + "type": "openapi" + }, + "type": "object" + }, + "TokenRequest": { + "allOf": [], + "properties": [ + { + "conflict": {}, + "generatedName": "tokenRequestClientId", + "key": "clientId", + "schema": { + "generatedName": "TokenRequestClientId", + "value": { + "schema": { + "type": "string" + }, + "generatedName": "TokenRequestClientId", + "groupName": [], + "type": "primitive" + }, + "groupName": [], + "type": "optional" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "tokenRequestClientSecret", + "key": "clientSecret", + "schema": { + "generatedName": "TokenRequestClientSecret", + "value": { + "schema": { + "type": "string" + }, + "generatedName": "TokenRequestClientSecret", + "groupName": [], + "type": "primitive" + }, + "groupName": [], + "type": "optional" + }, + "audiences": [] + } + ], + "allOfPropertyConflicts": [], + "generatedName": "TokenRequest", + "groupName": [], + "additionalProperties": false, + "source": { + "file": "../auth-api.yml", + "type": "openapi" + }, + "type": "object" + }, + "TokenResponse": { + "allOf": [], + "properties": [ + { + "conflict": {}, + "generatedName": "tokenResponseAccessToken", + "key": "accessToken", + "schema": { + "generatedName": "TokenResponseAccessToken", + "value": { + "schema": { + "type": "string" + }, + "generatedName": "TokenResponseAccessToken", + "groupName": [], + "type": "primitive" + }, + "groupName": [], + "type": "optional" + }, + "audiences": [] + } + ], + "allOfPropertyConflicts": [], + "generatedName": "TokenResponse", + "groupName": [], + "additionalProperties": false, + "source": { + "file": "../auth-api.yml", + "type": "openapi" + }, + "type": "object" + } + }, + "namespacedSchemas": {} + }, + "variables": {}, + "nonRequestReferencedSchemas": {}, + "securitySchemes": {}, + "globalHeaders": [], + "idempotencyHeaders": [], + "groups": {} +} \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/multi-api-environment-grouping-server-description.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/multi-api-environment-grouping-server-description.json new file mode 100644 index 000000000000..7ae859aa3d4f --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/multi-api-environment-grouping-server-description.json @@ -0,0 +1,224 @@ +{ + "absoluteFilePath": "/DUMMY_PATH", + "importedDefinitions": {}, + "namedDefinitionFiles": { + "__package__.yml": { + "absoluteFilepath": "/DUMMY_PATH", + "contents": { + "service": { + "auth": false, + "base-path": "", + "endpoints": { + "issueToken": { + "auth": undefined, + "display-name": "Issue a token", + "docs": undefined, + "examples": [ + { + "request": {}, + "response": { + "body": { + "accessToken": "accessToken", + }, + }, + }, + ], + "method": "POST", + "pagination": undefined, + "path": "/token", + "request": { + "body": { + "properties": { + "clientId": "optional", + "clientSecret": "optional", + }, + }, + "content-type": "application/json", + "headers": undefined, + "name": "TokenRequest", + "path-parameters": undefined, + "query-parameters": undefined, + }, + "response": { + "docs": "Successful response", + "status-code": 200, + "type": "TokenResponse", + }, + "source": { + "openapi": "../auth-api.yml", + }, + "url": "auth", + }, + "listWidgets": { + "auth": undefined, + "display-name": "List widgets", + "docs": undefined, + "examples": [ + { + "response": { + "body": [ + { + "id": "id", + "name": "name", + }, + ], + }, + }, + ], + "method": "GET", + "pagination": undefined, + "path": "/widgets", + "response": { + "docs": "Successful response", + "status-code": 200, + "type": "list", + }, + "source": { + "openapi": "../core-api.yml", + }, + "url": "api", + }, + }, + "source": { + "openapi": "../auth-api.yml", + }, + }, + "types": { + "TokenResponse": { + "docs": undefined, + "inline": undefined, + "properties": { + "accessToken": "optional", + }, + "source": { + "openapi": "../auth-api.yml", + }, + }, + "Widget": { + "docs": undefined, + "inline": undefined, + "properties": { + "id": "optional", + "name": "optional", + }, + "source": { + "openapi": "../core-api.yml", + }, + }, + }, + }, + "rawContents": "service: + auth: false + base-path: '' + endpoints: + listWidgets: + path: /widgets + method: GET + source: + openapi: ../core-api.yml + display-name: List widgets + response: + docs: Successful response + type: list + status-code: 200 + url: api + examples: + - response: + body: + - id: id + name: name + issueToken: + path: /token + method: POST + source: + openapi: ../auth-api.yml + display-name: Issue a token + request: + name: TokenRequest + body: + properties: + clientId: optional + clientSecret: optional + content-type: application/json + response: + docs: Successful response + type: TokenResponse + status-code: 200 + url: auth + examples: + - request: {} + response: + body: + accessToken: accessToken + source: + openapi: ../auth-api.yml +types: + Widget: + properties: + id: optional + name: optional + source: + openapi: ../core-api.yml + TokenResponse: + properties: + accessToken: optional + source: + openapi: ../auth-api.yml +", + }, + }, + "packageMarkers": {}, + "rootApiFile": { + "contents": { + "default-environment": "Production", + "default-url": "api", + "display-name": "Core API", + "environments": { + "Development": { + "urls": { + "api": "https://api.dev.example.com", + "auth": "https://auth.dev.example.com", + }, + }, + "Production": { + "urls": { + "api": "https://api.example.com", + "auth": "https://auth.example.com", + }, + }, + "Staging": { + "urls": { + "api": "https://api.stage.example.com", + "auth": "https://auth.stage.example.com", + }, + }, + }, + "error-discrimination": { + "strategy": "status-code", + }, + "name": "api", + }, + "defaultUrl": "api", + "rawContents": "name: api +error-discrimination: + strategy: status-code +display-name: Core API +environments: + Production: + urls: + api: https://api.example.com + auth: https://auth.example.com + Staging: + urls: + api: https://api.stage.example.com + auth: https://auth.stage.example.com + Development: + urls: + api: https://api.dev.example.com + auth: https://auth.dev.example.com +default-environment: Production +default-url: api +", + }, + "specVersion": "1.0.0", +} \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/auth-api.yml b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/auth-api.yml new file mode 100644 index 000000000000..0cfac24f924e --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/auth-api.yml @@ -0,0 +1,43 @@ +openapi: 3.0.0 +info: + title: Auth API + version: 1.0.0 +servers: + - url: https://auth.example.com + x-fern-server-name: Production + - url: https://auth.stage.example.com + x-fern-server-name: Staging + - url: https://auth.dev.example.com + x-fern-server-name: Development +paths: + /token: + post: + summary: Issue a token + operationId: issueToken + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/TokenRequest" + responses: + "200": + description: Successful response + content: + application/json: + schema: + $ref: "#/components/schemas/TokenResponse" +components: + schemas: + TokenRequest: + type: object + properties: + clientId: + type: string + clientSecret: + type: string + TokenResponse: + type: object + properties: + accessToken: + type: string diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/core-api.yml b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/core-api.yml new file mode 100644 index 000000000000..ae3b24c71bb9 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/core-api.yml @@ -0,0 +1,35 @@ +openapi: 3.0.0 +info: + title: Core API + version: 1.0.0 +servers: + - url: https://api.example.com + description: Core Unified API + x-fern-server-name: Production + - url: https://api.stage.example.com + x-fern-server-name: Staging + - url: https://api.dev.example.com + x-fern-server-name: Development +paths: + /widgets: + get: + summary: List widgets + operationId: listWidgets + responses: + "200": + description: Successful response + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Widget" +components: + schemas: + Widget: + type: object + properties: + id: + type: string + name: + type: string diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/fern.config.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/fern.config.json new file mode 100644 index 000000000000..c8911294210f --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/fern.config.json @@ -0,0 +1,4 @@ +{ + "organization": "seed", + "version": "0.0.0" +} diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/fern/generators.yml b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/fern/generators.yml new file mode 100644 index 000000000000..9f113f28dead --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/multi-api-environment-grouping-server-description/fern/generators.yml @@ -0,0 +1,9 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +api: + specs: + - openapi: ../core-api.yml + settings: + group-multi-api-environments: true + - openapi: ../auth-api.yml + settings: + group-multi-api-environments: true diff --git a/packages/cli/cli/changes/unreleased/fix-multi-api-environment-grouping-server-description.yml b/packages/cli/cli/changes/unreleased/fix-multi-api-environment-grouping-server-description.yml new file mode 100644 index 000000000000..37e725993464 --- /dev/null +++ b/packages/cli/cli/changes/unreleased/fix-multi-api-environment-grouping-server-description.yml @@ -0,0 +1,6 @@ +- summary: | + Fix `group-multi-api-environments` failing to produce multi-URL environments when a + server in one of the specs has a `description` (e.g. "Core Unified API") alongside an + explicit `x-fern-server-name`. Environment matching now prefers the explicit server + name over the description. + type: fix