Skip to content
Merged
9 changes: 9 additions & 0 deletions .devin/automation/sentry-triage/ledger/CLI-18.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"title": "Absolute OpenAPI filepath is not relative",
"disposition": "shipped",
"rationale": "Absolute OpenAPI spec paths are user configuration values and now resolve at the workspace-loading boundary instead of reaching RelativeFilePath.of.",
"fixSummary": "Resolve absolute OpenAPI spec paths directly during workspace loading.",
"prOrIssue": "",
"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."
}
9 changes: 9 additions & 0 deletions .devin/automation/sentry-triage/ledger/CLI-46.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"title": "Filepath is not relative (Linux absolute path)",
"disposition": "shipped",
"rationale": "Absolute OpenAPI spec paths are user configuration values and now resolve at the workspace-loading boundary instead of reaching RelativeFilePath.of.",
"fixSummary": "Resolve absolute OpenAPI spec paths directly during workspace loading.",
"prOrIssue": "",
"lastAnalyzed": "2026-05-16",
"problemSignature": "OpenAPI or workspace path conversion received an absolute path where a relative path was required; path boundary bug."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json

- summary: |
Fix workspace loading for OpenAPI specs configured with absolute file paths.
type: fix
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -71,6 +71,37 @@ describe("loadWorkspace", () => {
expect(workspace.didSucceed).toBe(true);
assert(workspace.didSucceed);
});

it("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 specs = await loadSingleNamespaceAPIWorkspace({
absolutePathToWorkspace: absolutePathToFixtures,
namespace: undefined,
definitions: [
{
schema: {
type: "oss",
path: absolutePathToOpenApi
},
origin: undefined,
overrides: undefined,
overlays: undefined,
audiences: [],
settings: undefined
}
]
});

expect(Array.isArray(specs)).toBe(true);
assert(Array.isArray(specs));
const spec = specs[0];
assert(spec != null);
expect(spec.type).toBe("openapi");
assert(spec.type === "openapi");
expect(spec.absoluteFilepath).toBe(absolutePathToOpenApi);
});
});

describe("loadWorkspace MISCONFIGURED_DIRECTORY", () => {
Expand Down
34 changes: 32 additions & 2 deletions packages/cli/workspace/loader/src/loadAPIWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,35 @@ import {
WorkspaceLoaderFailureType
} from "@fern-api/lazy-fern-workspace";
import { TaskContext } from "@fern-api/task-context";
import path from "path";
import { loadAPIChangelog } from "./loadAPIChangelog.js";

function resolveConfiguredFilepath({
absolutePathToWorkspace,
configuredFilepath
}: {
absolutePathToWorkspace: AbsoluteFilePath;
configuredFilepath: string;
}): AbsoluteFilePath {
if (path.isAbsolute(configuredFilepath)) {
return AbsoluteFilePath.of(configuredFilepath);
}
return join(absolutePathToWorkspace, RelativeFilePath.of(configuredFilepath));
}

function getFailureFilepath({
absolutePathToWorkspace,
configuredFilepath
}: {
absolutePathToWorkspace: AbsoluteFilePath;
configuredFilepath: string;
}): RelativeFilePath {
if (path.isAbsolute(configuredFilepath)) {
return RelativeFilePath.of(path.relative(absolutePathToWorkspace, configuredFilepath));
}
return RelativeFilePath.of(configuredFilepath);
}

export async function loadSingleNamespaceAPIWorkspace({
absolutePathToWorkspace,
namespace,
Expand Down Expand Up @@ -129,12 +156,15 @@ export async function loadSingleNamespaceAPIWorkspace({
continue;
}

const absoluteFilepath = join(absolutePathToWorkspace, RelativeFilePath.of(definition.schema.path));
const absoluteFilepath = resolveConfiguredFilepath({
absolutePathToWorkspace,
configuredFilepath: definition.schema.path
});
if (!(await doesPathExist(absoluteFilepath))) {
return {
didSucceed: false,
failures: {
[RelativeFilePath.of(definition.schema.path)]: {
[getFailureFilepath({ absolutePathToWorkspace, configuredFilepath: definition.schema.path })]: {
type: WorkspaceLoaderFailureType.FILE_MISSING
}
}
Expand Down
Loading