diff --git a/.devin/automation/sentry-triage/ledger/CLI-18.json b/.devin/automation/sentry-triage/ledger/CLI-18.json index 7b6be552e552..c7ce045a1856 100644 --- a/.devin/automation/sentry-triage/ledger/CLI-18.json +++ b/.devin/automation/sentry-triage/ledger/CLI-18.json @@ -1,9 +1,9 @@ { "title": "Absolute OpenAPI filepath is not relative", - "disposition": "keep_sentry", - "rationale": "Absolute path reached a relative-path invariant; needs path normalization or boundary validation fix.", - "fixSummary": "—", - "prOrIssue": "Keep in Sentry until product fix", - "lastAnalyzed": "2026-05-04", + "disposition": "shipped", + "rationale": "Absolute OpenAPI spec paths are user configuration values and now fail at the workspace-loading boundary as a non-reportable config error instead of reaching RelativeFilePath.of.", + "fixSummary": "Reject absolute OpenAPI spec paths during workspace loading with a user-facing config error.", + "prOrIssue": "https://github.com/fern-api/fern/pull/15953", + "lastAnalyzed": "2026-05-16", "problemSignature": "OpenAPI or workspace path conversion received an absolute path where a relative path was required; path boundary bug until fixed." } diff --git a/.devin/automation/sentry-triage/ledger/CLI-46.json b/.devin/automation/sentry-triage/ledger/CLI-46.json index d8745c6bdd2f..dea66091554e 100644 --- a/.devin/automation/sentry-triage/ledger/CLI-46.json +++ b/.devin/automation/sentry-triage/ledger/CLI-46.json @@ -1,9 +1,9 @@ { "title": "Filepath is not relative (Linux absolute path)", - "disposition": "keep_sentry", - "rationale": "Absolute path reached a relative-path invariant; same family as CLI-3J and CLI-18.", - "fixSummary": "—", - "prOrIssue": "Keep in Sentry until product fix", + "disposition": "shipped", + "rationale": "Absolute OpenAPI spec paths are user configuration values and now fail at the workspace-loading boundary as a non-reportable config error instead of reaching RelativeFilePath.of.", + "fixSummary": "Reject absolute OpenAPI spec paths during workspace loading with a user-facing config error.", + "prOrIssue": "https://github.com/fern-api/fern/pull/15953", "lastAnalyzed": "2026-05-16", "problemSignature": "OpenAPI or workspace path conversion received an absolute path where a relative path was required; path boundary bug." } diff --git a/packages/cli/cli/changes/unreleased/fix-absolute-openapi-paths.yml b/packages/cli/cli/changes/unreleased/fix-absolute-openapi-paths.yml new file mode 100644 index 000000000000..6e2b09d1d3a1 --- /dev/null +++ b/packages/cli/cli/changes/unreleased/fix-absolute-openapi-paths.yml @@ -0,0 +1,5 @@ +# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json + +- summary: | + Report absolute OpenAPI spec paths as user-facing workspace configuration errors. + type: fix diff --git a/packages/cli/workspace/lazy-fern-workspace/src/utils/Result.ts b/packages/cli/workspace/lazy-fern-workspace/src/utils/Result.ts index 140f0a03737e..8b6076777aa9 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/utils/Result.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/utils/Result.ts @@ -23,6 +23,7 @@ export declare namespace WorkspaceLoader { | FileReadFailure | FileParseFailure | MissingFileFailure + | AbsoluteFilepathFailure | StructureValidationFailure | DependencyFailure | JsonSchemaValidationFailure; @@ -41,6 +42,11 @@ export declare namespace WorkspaceLoader { type: WorkspaceLoaderFailureType.FILE_MISSING; } + export interface AbsoluteFilepathFailure { + type: WorkspaceLoaderFailureType.ABSOLUTE_FILEPATH; + filepath: string; + } + export interface MisconfiguredDirectoryFailure { type: WorkspaceLoaderFailureType.MISCONFIGURED_DIRECTORY; } @@ -87,6 +93,7 @@ export enum WorkspaceLoaderFailureType { FILE_READ = "FILE_READ", FILE_PARSE = "FILE_PARSE", FILE_MISSING = "FILE_MISSING", + ABSOLUTE_FILEPATH = "ABSOLUTE_FILEPATH", STRUCTURE_VALIDATION = "STRUCTURE_VALIDATION", JSONSCHEMA_VALIDATION = "JSONSCHEMA_VALIDATION", DEPENDENCY_NOT_LISTED = "DEPENDENCY_NOT_LISTED", diff --git a/packages/cli/workspace/lazy-fern-workspace/src/utils/handleFailedWorkspaceParserResult.ts b/packages/cli/workspace/lazy-fern-workspace/src/utils/handleFailedWorkspaceParserResult.ts index 29d7207d2cb0..ac5ba7766cda 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/utils/handleFailedWorkspaceParserResult.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/utils/handleFailedWorkspaceParserResult.ts @@ -52,6 +52,11 @@ function handleWorkspaceParserFailureForFile({ case WorkspaceLoaderFailureType.FILE_MISSING: logger.error("Missing file: " + relativeFilepath); break; + case WorkspaceLoaderFailureType.ABSOLUTE_FILEPATH: + logger.error( + `OpenAPI spec paths must be relative to the Fern workspace. Found absolute path: ${failure.filepath}` + ); + break; case WorkspaceLoaderFailureType.FILE_PARSE: if (failure.error instanceof YAMLException) { logger.error( diff --git a/packages/cli/workspace/loader/src/__test__/loadWorkspace.test.ts b/packages/cli/workspace/loader/src/__test__/loadWorkspace.test.ts index f7473663c324..90ea3ae83190 100644 --- a/packages/cli/workspace/loader/src/__test__/loadWorkspace.test.ts +++ b/packages/cli/workspace/loader/src/__test__/loadWorkspace.test.ts @@ -6,7 +6,7 @@ import { createMockTaskContext } from "@fern-api/task-context"; import assert from "assert"; import { handleFailedWorkspaceParserResult } from "../handleFailedWorkspaceParserResult.js"; -import { loadAPIWorkspace } from "../loadAPIWorkspace.js"; +import { loadAPIWorkspace, loadSingleNamespaceAPIWorkspace } from "../loadAPIWorkspace.js"; function createCapturingLogger(): Logger & { errors: string[]; debugs: string[] } { const errors: string[] = []; @@ -71,6 +71,38 @@ describe("loadWorkspace", () => { expect(workspace.didSucceed).toBe(true); assert(workspace.didSucceed); }); + + it("rejects open api with absolute spec path", async () => { + const absolutePathToFixtures = join(AbsoluteFilePath.of(__dirname), RelativeFilePath.of("fixtures")); + const absolutePathToOpenApi = join(absolutePathToFixtures, RelativeFilePath.of("openapi.yml")); + + const result = await loadSingleNamespaceAPIWorkspace({ + absolutePathToWorkspace: absolutePathToFixtures, + namespace: undefined, + definitions: [ + { + schema: { + type: "oss", + path: absolutePathToOpenApi + }, + origin: undefined, + overrides: undefined, + overlays: undefined, + audiences: [], + settings: undefined + } + ] + }); + + expect(Array.isArray(result)).toBe(false); + assert(!Array.isArray(result)); + expect(result.didSucceed).toBe(false); + assert(!result.didSucceed); + expect(result.failures[RelativeFilePath.of("generators.yml")]).toEqual({ + type: WorkspaceLoaderFailureType.ABSOLUTE_FILEPATH, + filepath: absolutePathToOpenApi + }); + }); }); describe("loadWorkspace MISCONFIGURED_DIRECTORY", () => { diff --git a/packages/cli/workspace/loader/src/handleFailedWorkspaceParserResult.ts b/packages/cli/workspace/loader/src/handleFailedWorkspaceParserResult.ts index 8a8df15174d6..1c9f0a3e8f99 100644 --- a/packages/cli/workspace/loader/src/handleFailedWorkspaceParserResult.ts +++ b/packages/cli/workspace/loader/src/handleFailedWorkspaceParserResult.ts @@ -51,6 +51,11 @@ function handleWorkspaceParserFailureForFile({ case WorkspaceLoaderFailureType.FILE_MISSING: logger.error("Missing file: " + relativeFilepath); break; + case WorkspaceLoaderFailureType.ABSOLUTE_FILEPATH: + logger.error( + `OpenAPI spec paths must be relative to the Fern workspace. Found absolute path: ${failure.filepath}` + ); + break; case WorkspaceLoaderFailureType.FILE_PARSE: if (failure.error instanceof YAMLException) { logger.error( diff --git a/packages/cli/workspace/loader/src/loadAPIWorkspace.ts b/packages/cli/workspace/loader/src/loadAPIWorkspace.ts index e10ef1cc2b11..ae1d23adf114 100644 --- a/packages/cli/workspace/loader/src/loadAPIWorkspace.ts +++ b/packages/cli/workspace/loader/src/loadAPIWorkspace.ts @@ -15,6 +15,7 @@ import { WorkspaceLoaderFailureType } from "@fern-api/lazy-fern-workspace"; import { TaskContext } from "@fern-api/task-context"; +import path from "path"; import { loadAPIChangelog } from "./loadAPIChangelog.js"; export async function loadSingleNamespaceAPIWorkspace({ @@ -148,12 +149,25 @@ export async function loadSingleNamespaceAPIWorkspace({ continue; } - const absoluteFilepath = join(absolutePathToWorkspace, RelativeFilePath.of(definition.schema.path)); + if (path.isAbsolute(definition.schema.path)) { + return { + didSucceed: false, + failures: { + [RelativeFilePath.of(GENERATORS_CONFIGURATION_FILENAME)]: { + type: WorkspaceLoaderFailureType.ABSOLUTE_FILEPATH, + filepath: definition.schema.path + } + } + }; + } + + const relativeFilepath = RelativeFilePath.of(definition.schema.path); + const absoluteFilepath = join(absolutePathToWorkspace, relativeFilepath); if (!(await doesPathExist(absoluteFilepath))) { return { didSucceed: false, failures: { - [RelativeFilePath.of(definition.schema.path)]: { + [relativeFilepath]: { type: WorkspaceLoaderFailureType.FILE_MISSING } }