feat(cli): add git-ref-based docs versioning - #17288
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| targetAudiences, | ||
| buildTranslatedApiDefinitions = false | ||
| buildTranslatedApiDefinitions = false, | ||
| buildRefVersions = true, |
There was a problem hiding this comment.
🔴 Documentation validation unexpectedly rebuilds historical versions and can fail
Historical git-ref-backed versions are built by materializing git checkouts during validation and definition-writing (because ref building is left on by default at packages/cli/docs-resolver/src/DocsDefinitionResolver.ts:221) rather than only at publish, so fern check and related commands perform git operations they were never meant to and can error out.
Impact: A project that declares a tag/branch-based docs version can no longer run fern check (or the write-definition/redirect-check flows) reliably — it triggers slow git worktree checkouts and hard-fails in common CI setups, and even the publish flow's validation step can abort before publishing.
Why buildRefVersions defaults on for validation call sites
The resolver defaults buildRefVersions = true (packages/cli/docs-resolver/src/DocsDefinitionResolver.ts:221), and it is threaded into parseDocsConfiguration (packages/cli/docs-resolver/src/DocsDefinitionResolver.ts:459) which then materializes each ref version via materializeGitRef (git rev-parse / fetch / git worktree add).
The publish path (packages/cli/generation/remote-generation/remote-workspace-runner/src/publishDocs.ts:544) and preview path (packages/cli/docs-preview/src/previewDocs.ts) were updated to pass cliVersion, but the validation/utility call sites were not and do not opt out of ref building:
packages/cli/yaml/docs-validator/src/rules/valid-markdown-link/valid-markdown-link.ts:80packages/cli/yaml/docs-validator/src/rules/missing-redirects/missing-redirects.ts:127packages/cli/cli/src/commands/write-docs-definition/writeDocsDefinitionForProject.ts:35
Both rules are registered in getAllRules.ts, so fern check runs them. Each constructs DocsDefinitionResolver without buildRefVersions, so it defaults to true and materializes every ref version. Two concrete failures follow:
- In shallow/tagless CI checkouts
resolveRefToShathrows the actionableConfigError— validation fails even though the PR's stated intent is that ref versions are validated at build time, not during current-tree validation. - If a ref version contains an
api:section,resolveApiWorkspaces(packages/cli/docs-resolver/src/DocsDefinitionResolver.ts:1185-1192) throws anInternalErrorbecausecliVersionisundefinedat these call sites.
The docs-validator AST change only skipped current-tree path validation for ref entries; it did not prevent the resolver-backed rules from fully building them.
Prompt for agents
The DocsDefinitionResolver constructor defaults buildRefVersions to true so the publish path (which passes cliVersion) builds git-ref-backed versions. However three other construction sites inherit this default and neither opt out nor pass cliVersion: the docs-validator rules valid-markdown-link.ts (line ~80) and missing-redirects.ts (line ~127), and writeDocsDefinitionForProject.ts (line ~35). Because these run during `fern check` and pre-publish validation, they now materialize git worktrees for every ref version and hard-fail in shallow/tagless CI, and additionally throw an InternalError from resolveApiWorkspaces when a ref version has an `api:` section (cliVersion is undefined there). The PR's design intent is that ref versions are validated at build/publish time, not during current-tree validation. Fix by making these validation/utility call sites explicitly pass buildRefVersions: false (matching the docs-validator AST change that already skips ref entries), so validation only exercises the working-tree versions. Confirm whether writeDocsDefinitionForProject should build refs (and if so thread cliVersion through it); otherwise set it false as well.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Good catch, fixed in 559ddf4. The three validation/utility construction sites (valid-markdown-link.ts, missing-redirects.ts, writeDocsDefinitionForProject.ts) now explicitly pass buildRefVersions: false, so fern check and write-definition only exercise working-tree versions and never materialize git worktrees or hit the cliVersion == null path. Ref versions are still built at publish (publishDocs.ts) and preview-with---versions all, matching the design intent that ref versions are validated at build time.
| const result = await runGit({ | ||
| args: ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], | ||
| cwd: repoRoot, | ||
| context | ||
| }); | ||
| if (result.exitCode !== 0 || result.stdout.length === 0) { | ||
| return undefined; | ||
| } | ||
| return result.stdout; | ||
| } |
There was a problem hiding this comment.
🟨 Git ref from docs.yml passed to git without option terminator
The tag/branch value from docs.yml is passed directly as a positional argument to git subcommands (git rev-parse --verify --quiet ${ref}^{commit} at packages/cli/configuration-loader/src/docs-yml/git-versions/materializeGitRef.ts:100, and git fetch ... ${remote} ${ref} at packages/cli/configuration-loader/src/docs-yml/git-versions/materializeGitRef.ts:134) without a -- end-of-options separator. A ref value beginning with - (e.g. --upload-pack=...) could be interpreted by git as an option rather than a ref, enabling git option-injection. Commands are invoked via execa with an argv array (no shell), so classic shell command injection does not apply.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Hardened in 559ddf4 — added a -- end-of-options terminator before the ref in both git rev-parse --verify --quiet -- <ref>^{commit} and git fetch ... <remote> -- <ref>, so a ref value beginning with - can no longer be interpreted as a git flag. (As you note, execa uses an argv array with no shell, so there was never a shell-injection vector — this closes the git option-injection one.)
…ld in validation call sites; harden git ref args Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…treat ref as pathspec 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>
…right doesn't resolve root tsconfig extends Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Docs Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on
Docs generation runs |
SDK Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on Full benchmark table (click to expand)
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 |
A declared ref (e.g. release/2.3) present only as origin/<ref> after a normal clone failed to resolve because git does not fall through from a bare name to refs/remotes/<remote>/<ref>. Also resolve FETCH_HEAD after a targeted fetch so shallow/single-branch CI checkouts can backfill the ref. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…issing library, mutable/immutable resolution) - Replace version tag:/branch: with a single ref: field (branch, tag, or SHA), matching the generators.yml git-source convention; regenerate schemas. - Drop explicit path: on ref-backed versions; content root resolves from the ref's versions[0].path then top-level navigation. - Revert library sections to warn-and-skip on missing config/output (keeps the behavior consistent with working-tree docs; no breaking change bundled in). - Resolve mutable branch refs from the freshest remote commit while keeping tags/SHAs local-first; share isCommitSha/isGitAvailable via core-utils. Co-Authored-By: Claude <noreply@anthropic.com>
…rn docs dev --versions Remove the `fern docs dev --versions current|all` flag and its dev/preview-path plumbing (buildRefVersions/cliVersion/cliName through devDocsWorkspace, previewDocs, runPreviewServer, runAppPreviewServer). `fern docs dev` now always previews the working-tree version only; git-ref-backed versions are materialized on the publish path. Local preview of ref-backed versions can be re-added as a follow-up. Co-Authored-By: Claude <noreply@anthropic.com>
…n up worktree temp dirs - materializeGitRef now validates the fern folder exists in the materialized worktree and throws a CliError naming the ref, sha, and expected path instead of leaking a raw ENOENT on docs.yml with a temp path. - Register tmp graceful cleanup so materialized worktree temp dirs are removed on process exit; the start-of-run 'git worktree prune' then reaps the stale registrations. Verified steady-state (no unbounded /tmp or .git/worktrees growth). - Add a real-git end-to-end matrix test covering ref resolution, mutability, caching, environment, content-root selection, and the fern-folder-moved case.
A git-ref-backed version derives its content root from the ref itself, so a current-branch 'path:' has no effect. Fail with an actionable config error instead of silently ignoring 'path:'. The check runs regardless of buildRefVersions so 'fern check' surfaces it too.
Adds a valid-version-ref docs-validator rule so fern check surfaces the ref+path config error (the parser-level guard only fired at build time). Trims redundant git-ref tests (materializeGitRef/getVersionContentRef are covered by the matrix). 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>
|
LGTM, but a small nit on the interface. It seems this is the current use case:
However, @Ryan-Amirthan why have a version flag at all? If multiple versions are provided, isn't the default to assume we'd want all - then the flag can be used to narrow down specific versions for some deploys? |
agreed @aditya-arolkar-swe , stale code that should have been removed when we changed the scope to remove version specific publishing. |
…ing docstrings Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Re: @aditya-arolkar-swe's interface nit + @Ryan-Amirthan's reply — agreed, no version flag. Correcting my earlier wording here: Current behavior:
|
Thanks - the usual
@Ryan-Amirthan why would we not want them previewing this with Regardless, looks good now. Can merge and address above in follow up PR. |
|
@aditya-arolkar-swe on the "why can't Agreed this is a clean follow-up. If Ryan wants it, I'm happy to open that PR (restore dev preview, default to all versions, no flag — matching the "default = all" idea from your earlier comment). |
@aditya-arolkar-swe removed previewing from fern docs dev because I did not want |
Description
Adds v1 of git-ref-based docs versioning to the CLI. A version entry in
docs.ymlcan name a singleref:(a git branch, tag, or commit SHA) instead of committing that version's content into the working tree. On publish, the CLI resolves the ref to a commit, materializes the repo at that commit, and composes the historical pages/nav/assets/API/libraries with the current branch's theme, shell, andversions:metadata.path:-only entries are byte-for-byte unchanged.Changes Made
docs.ymlVersionConfiggains an optionalref:(schema + generated SDK/jsonschema regenerated). The field shape is isolated ingetVersionContentRefso it can change in one spot.ref:orpath:, never both — declaring both is a config error surfaced by bothfern check(thevalid-version-refrule) and the publish/build path with an actionable message.git-versions/:materializeGitRef(resolve<ref>^{commit}, backfill shallow/tagless CI history,git worktreematerialize the whole repo, cache by SHA, prune + graceful temp cleanup) andresolveRefContentRoot(content-root precedence: refversions[0].path→ ref top-levelnavigation:, no recursion).DocsDefinitionResolverthreads aVersionContentSourceso historical pages/assets/snippets/API+OSS workspaces/libraries:output resolve from the ref.fern docs devpreviews only the working-tree version (git-ref-backed versions are materialized on publish);fern checknever materializes refs._navigation.yml, missing generated MDX) — a historical version can publish without its SDK reference. This matches pre-existing single-version behavior; unlikeapi:sections it is not a hard failure. (An earlier hard-fail proposal was intentionally not shipped; revisit in v2.)CliErrornaming the ref, SHA, and expected path (instead of a rawENOENT).origin/<ref>and resolvesFETCH_HEADafter a targeted fetch (covers shallow/single-branch CI checkouts);--end-of-optionshardens refs beginning with-.docs-preview-smoke-test/playwright/tsconfig.jsonso a newer Playwright doesn't walk up to the workspace-roottsconfig.json(unrelated harness fix that was blocking the smoke-test).Scope deliberately left out (per prompt)
ref:on library inputs, no per-version library overrides,fern generate --docsdoes not invoke md generation. (v2.)Testing
getVersionContentRef,resolveRefContentRootprecedence + actionable error,refPathMutualExclusion,valid-version-ref, and a git-integration matrix formaterializeGitRef(remote-tracking branch, tag, unresolvable ref, shallow/tagless fetch, leading-dash ref, mutability, same-SHA caching, offline fallback, fern-folder-moved).fern-api/ryanstep-config(branch + tag) built with this CLI; publish composes all three versions and serves/welcome,/2-0/welcome,/1-0/welcome.Link to Devin session: https://app.devin.ai/sessions/185c0e74b50b43f79023c793c814ac44