Skip to content

feat(cli): allow docs.yml redirects to reference a YAML file - #17311

Open
devin-ai-integration[bot] wants to merge 4 commits into
mainfrom
devin/1785509387-redirects-filepath
Open

feat(cli): allow docs.yml redirects to reference a YAML file#17311
devin-ai-integration[bot] wants to merge 4 commits into
mainfrom
devin/1785509387-redirects-filepath

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Description

redirects in docs.yml now accepts either the inline list it accepts today or a relative/absolute filepath to a YAML file that contains only a redirects list:

# fern/docs.yml
redirects: ./redirects.yml
# fern/redirects.yml
redirects:
  - source: /old-plants
    destination: /plants
  - source: /plants/:plantId/legacy
    destination: /plants/:plantId
    permanent: false

The file is read and validated at docs-workspace load time, so every downstream consumer (parseDocsConfiguration, no-circular-redirects, missing-redirects, valid-markdown-link, theme stitching, publish) keeps seeing a plain list — no consumer had to learn about the filepath form.

Changes Made

  • fern/apis/docs-yml/definition/docs.yml: redirects: optional<RedirectsConfiguration> where RedirectsConfiguration is an undiscriminated union of list<RedirectConfig> | string; regenerated the docs-config SDK and the JSON schemas.
  • New resolveRedirects (packages/cli/configuration-loader): resolves the path relative to docs.yml, parses the YAML, and validates it against RedirectsFile — a strict object with a single redirects key holding strict RedirectConfigs. Errors on a missing file/non-file, malformed YAML, a bare list with no redirects key, or an invalid redirect.
  • loadRawDocsConfiguration, parseDocsConfiguration, and cli-v2's LegacyDocsWorkspaceAdapter call it; DocsWorkspace.config / the docs-validator AST node are now typed DocsConfigurationWithResolvedRedirects (redirects?: RedirectConfig[]).

Testing

  • Unit tests added/updated — packages/cli/configuration-loader/src/docs-yml/__test__/resolveRedirects.test.ts (inline passthrough, relative + absolute file loading, missing file, empty path, invalid redirect, bare list without the redirects key).
  • Manual testing completed with the built CLI on a scratch project:
# redirects: ./redirects.yml with a `redirects:` root key
Found 0 errors and 1 warning

# circular redirects inside the file — still caught by no-circular-redirects
[error] Circular redirect chain detected: /a -> /b -> /a

# invalid entry in the file
Failed to parse /tmp/rt/fern/redirects.yml. The file must contain only a `redirects` list:
  - redirects.0.destination: Invalid input: expected string, received undefined

# bare list with no `redirects` key
Failed to parse /tmp/rt/fern/redirects.yml. The file must contain only a `redirects` list:
  - : Invalid input: expected object, received array

# file missing
Failed to load redirects: /tmp/rt/fern/redirects.yml is not a file

# inline list (regression) — Found 0 errors and 1 warning

Docs for the new form live in fern-api/docs and aren't updated here — happy to open that PR too.

Link to Devin session: https://app.devin.ai/sessions/70d7967abc644ccaa6292cb3cf80bf73


Open in Devin Review

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 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.

Reviewed the changes — everything looks good. No issues found.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines +32 to +38
const absoluteFilepathToRedirects = resolve(dirname(absoluteFilepathToDocsConfig), RelativeFilePath.of(redirects));
if (!(await doesPathExist(absoluteFilepathToRedirects))) {
throw new CliError({
message: `Failed to load redirects: ${absoluteFilepathToRedirects} does not exist`,
code: CliError.Code.ParseError
});
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🟡 Pointing redirects at an absolute file path crashes the CLI with an internal error

The redirects path from docs.yml is required to be relative (RelativeFilePath.of(redirects) at packages/cli/configuration-loader/src/docs-yml/resolveRedirects.ts:32) before any friendly error handling runs, so users who give a full path see an internal crash instead of a clear message.
Impact: The command aborts with a raw internal error and no guidance about what is wrong in the configuration file.

Mechanism: RelativeFilePath.of throws a plain Error for absolute inputs, bypassing CliError handling

RelativeFilePath.of throws new Error("Filepath is not relative: " + value) when given an absolute path (packages/commons/fs-utils/src/RelativeFilePath.ts:11-13). resolveRedirects is invoked during workspace load (packages/cli/workspace/loader/src/loadDocsWorkspace.ts:124-127) and in parseDocsConfiguration, neither of which converts that generic error into a CliError, so every command that loads docs (check, generate, docs dev) fails with an unexpected-error stack rather than the intended Failed to load redirects: ... message.

The rest of the docs config resolves user-supplied paths with resolve(dirname(absoluteFilepathToDocsConfig), unresolvedFilepath) (packages/cli/configuration-loader/src/docs-yml/parseDocsConfiguration.ts:1694-1702), which tolerates absolute inputs; resolveRedirects deviates from that convention.

A related edge case: redirects: "" resolves to the containing directory, doesPathExist returns true, and readFile then throws an uncaught EISDIR.

Suggested change
const absoluteFilepathToRedirects = resolve(dirname(absoluteFilepathToDocsConfig), RelativeFilePath.of(redirects));
if (!(await doesPathExist(absoluteFilepathToRedirects))) {
throw new CliError({
message: `Failed to load redirects: ${absoluteFilepathToRedirects} does not exist`,
code: CliError.Code.ParseError
});
}
const absoluteFilepathToRedirects = resolve(dirname(absoluteFilepathToDocsConfig), redirects);
if (!(await doesPathExist(absoluteFilepathToRedirects))) {
throw new CliError({
message: `Failed to load redirects: ${absoluteFilepathToRedirects} does not exist`,
code: CliError.Code.ParseError
});
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed in bf96a5b. resolveRedirects now uses resolve(dirname(docsConfig), redirects) directly (matching resolveFilepath), so absolute paths work instead of throwing a raw Filepath is not relative error. Also guarded the redirects: "" case and switched to doesPathExist(..., "file") so a directory reports is not a file rather than blowing up with EISDIR. Tests added for both.

willkendall01 and others added 3 commits July 31, 2026 14:57
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.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) 264.8s (35 versions) +8.1s (+3.2%)

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 16:04 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 66s -14s (-17.5%)
go-sdk square 143s (n=5) 308s (n=5) 136s -7s (-4.9%)
java-sdk square 225s (n=5) 287s (n=5) 204s -21s (-9.3%)
php-sdk square 78s (n=5) N/A 55s -23s (-29.5%)
python-sdk square 143s (n=5) 259s (n=5) 142s -1s (-0.7%)
ruby-sdk-v2 square 109s (n=5) 151s (n=5) 86s -23s (-21.1%)
rust-sdk square 213s (n=5) 215s (n=5) 194s -19s (-8.9%)
swift-sdk square 78s (n=5) 457s (n=5) 56s -22s (-28.2%)
ts-sdk square 178s (n=5) 188s (n=5) 96s -82s (-46.1%)

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 16:05 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