Skip to content

fix(cli): allow docs instances on Fern-managed Cloudflare zones - #17603

Open
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1788220453-fern-managed-docs-domains
Open

fix(cli): allow docs instances on Fern-managed Cloudflare zones#17603
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1788220453-fern-managed-docs-domains

Conversation

@devin-ai-integration

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

Copy link
Copy Markdown
Contributor

Description

fern check / fern generate --docs rejects any instances[].url that doesn't end in docs.buildwithfern.com or docs.dev.buildwithfern.com, so docs sites on the other Fern-managed Cloudflare zones (e.g. adi-tests-new-pipeline.buildwithfern.dev) fail validation even though the docs publishing pipeline serves them. This adds those zones — buildwithfern.dev, fernapi.dev, ferndocs.com, ferndocs.dev, fernmcp.dev, all Fern-owned/Cloudflare-managed — to the CLI's allowlist.

Matching is now label-aware instead of a bare endsWith, so lookalikes like notbuildwithfern.dev or x.buildwithfern.dev.evil.com get the accurate "must end with one of ..." error rather than the misleading "a subdomain is required":

// before: hostname.endsWith(domain)
// after:  hostname === domain || hostname.endsWith("." + domain)   // longest match wins

Subdomain rules (single label, ≤62 chars, alphanumerics + hyphens) are unchanged and apply to every zone.

Changes Made

  • VALID_FERN_DOMAINS in packages/cli/yaml/docs-validator/src/rules/valid-instance-url/valid-instance-url.ts gains the five Cloudflare-managed zones; existing two are kept.
  • Domain matching requires a label boundary; the "subdomain required" error is now only for a bare apex domain.
  • CLI changelog entry under packages/cli/cli/changes/unreleased/.

Testing

  • Unit tests added/updated — new accept cases per zone (including adi-tests-new-pipeline.buildwithfern.dev), plus rejection cases for unrelated domains (mycompany.buildwithfern.com, mycompany.fernapi.com), lookalikes (notbuildwithfern.dev, myferndocs.dev, x.ferndocs.com.evil.com), and apex-without-subdomain for every zone.
  • pnpm turbo run test --filter @fern-api/docs-validator → 226 passed; pnpm turbo run compile --filter @fern-api/docs-validator, pnpm lint:biome, pnpm format:fix clean.

Link to Devin session: https://app.devin.ai/sessions/b3c4f788f19a48c58b286827f949023c
Open in Devin Desktop: https://app.devin.ai/desktop/session/b3c4f788f19a48c58b286827f949023c?variant=devin


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.

AI Review Summary

Adds five Fern-managed Cloudflare zones to the docs instance URL allowlist and makes domain matching label-aware. Logic is correct; only minor nits around the redundant array copy/sort per call and a subtle interaction between the new buildwithfern.dev zone and the existing subdomain rules.

  • 🔵 1 suggestion(s)

To request another review, comment /ai-review on this pull request.

Comment on lines +62 to +64
const matchedDomain = [...VALID_FERN_DOMAINS]
.sort((a, b) => b.length - a.length)
.find((domain) => hostname === domain || hostname.endsWith("." + domain));

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 copy + sort runs on every call. Since VALID_FERN_DOMAINS is a module constant, sort once at module scope (and note that longest-match only matters for docs.dev.buildwithfern.com vs docs.buildwithfern.com, which aren't actually suffixes of each other at a label boundary — x.docs.dev.buildwithfern.com doesn't end in .docs.buildwithfern.com. So the sort is defensive rather than required, which is fine, but hoist it):

Suggested change
const matchedDomain = [...VALID_FERN_DOMAINS]
.sort((a, b) => b.length - a.length)
.find((domain) => hostname === domain || hostname.endsWith("." + domain));
const matchedDomain = SORTED_FERN_DOMAINS.find(
(domain) => hostname === domain || hostname.endsWith("." + domain)
);

with const SORTED_FERN_DOMAINS = [...VALID_FERN_DOMAINS].sort((a, b) => b.length - a.length); next to the constant.

@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: No Issues Found

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

Devin Review

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