Skip to content

Commit c71a27f

Browse files
committed
docs: guard Fern release version content
1 parent 12b89ea commit c71a27f

4 files changed

Lines changed: 92 additions & 6 deletions

File tree

fern/scripts/fern-release-version.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from pathlib import Path
1414

1515
VERSION_RE = re.compile(r"\d+\.\d+\.\d+(?:[-.][0-9A-Za-z]+)*")
16+
AS_OF_VERSION_RE = re.compile(rf"As of Data Designer\s+\[?v?({VERSION_RE.pattern})")
17+
NAV_PATH_RE = re.compile(r"^\s*path:\s+\./([^#\s]+)\s*$")
1618

1719

1820
class ReleaseVersionError(RuntimeError):
@@ -33,6 +35,22 @@ def version_slug(version: str) -> str:
3335
return f"v{normalize_version(version)}"
3436

3537

38+
def version_key(value: str) -> tuple[int, int, int, int, str]:
39+
version = normalize_version(value)
40+
match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)(.*)", version)
41+
if not match:
42+
raise ReleaseVersionError(f"Invalid version '{value}'")
43+
suffix = match.group(4)
44+
return (int(match.group(1)), int(match.group(2)), int(match.group(3)), int(not suffix), suffix)
45+
46+
47+
def parse_yaml_value(value: str) -> str:
48+
value = value.strip()
49+
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
50+
return value[1:-1]
51+
return value
52+
53+
3654
def find_top_level_block(lines: list[str], name: str) -> tuple[int, int]:
3755
start = next((i for i, line in enumerate(lines) if line == f"{name}:\n"), -1)
3856
if start == -1:
@@ -59,11 +77,77 @@ def versions_block_text(root: Path) -> str:
5977
return "".join(lines[start:end])
6078

6179

80+
def version_entries(root: Path) -> list[dict[str, str]]:
81+
entries: list[dict[str, str]] = []
82+
current: dict[str, str] = {}
83+
for line in versions_block_text(root).splitlines():
84+
stripped = line.strip()
85+
if stripped.startswith("- display-name:"):
86+
if current:
87+
entries.append(current)
88+
current = {"display_name": parse_yaml_value(stripped.split(":", 1)[1])}
89+
elif current and stripped.startswith("path:"):
90+
current["path"] = parse_yaml_value(stripped.split(":", 1)[1])
91+
elif current and stripped.startswith("slug:"):
92+
current["slug"] = parse_yaml_value(stripped.split(":", 1)[1])
93+
if current:
94+
entries.append(current)
95+
return entries
96+
97+
98+
def highest_version_slug(entries: list[dict[str, str]]) -> str | None:
99+
slugs = [entry["slug"] for entry in entries if re.fullmatch(rf"v{VERSION_RE.pattern}", entry.get("slug", ""))]
100+
return max(slugs, key=version_key, default=None)
101+
102+
62103
def has_version_entry(root: Path, slug: str) -> bool:
63104
block = versions_block_text(root)
64105
return re.search(rf"^\s+slug:\s+{re.escape(slug)}\s*$", block, re.MULTILINE) is not None
65106

66107

108+
def check_latest_display_name(root: Path) -> list[str]:
109+
entries = version_entries(root)
110+
latest = next((entry for entry in entries if entry.get("slug") == "latest"), None)
111+
highest = highest_version_slug(entries)
112+
if latest is None or highest is None:
113+
return []
114+
115+
match = re.search(rf"\bv({VERSION_RE.pattern})\b", latest.get("display_name", ""))
116+
if not match:
117+
return ["Latest version display name must include the latest registered version slug"]
118+
119+
displayed = f"v{match.group(1)}"
120+
if displayed != highest:
121+
return [f"Latest display name points at {displayed}, but highest registered version is {highest}"]
122+
return []
123+
124+
125+
def referenced_mdx_paths(nav: Path) -> list[Path]:
126+
versions_dir = nav.parent
127+
paths: list[Path] = []
128+
for line in nav.read_text().splitlines():
129+
match = NAV_PATH_RE.match(line)
130+
if match:
131+
path = versions_dir / match.group(1)
132+
if path.suffix == ".mdx" and path.exists():
133+
paths.append(path)
134+
return paths
135+
136+
137+
def check_as_of_versions(root: Path) -> list[str]:
138+
errors: list[str] = []
139+
for nav in sorted((root / "versions").glob("v*.yml")):
140+
nav_slug = nav.stem
141+
nav_version = version_key(nav_slug)
142+
for path in referenced_mdx_paths(nav):
143+
for match in AS_OF_VERSION_RE.finditer(path.read_text()):
144+
content_slug = version_slug(match.group(1))
145+
if version_key(content_slug) > nav_version:
146+
rel_path = path.relative_to(root)
147+
errors.append(f"{nav.name} references {rel_path}, which declares {content_slug}")
148+
return errors
149+
150+
67151
def update_docs_yml(root: Path, slug: str) -> None:
68152
docs_yml = root / "docs.yml"
69153
lines = read_docs_lines(root)
@@ -149,6 +233,8 @@ def check_release(root: Path, slug: str) -> list[str]:
149233
elif "navigation:" not in nav.read_text():
150234
errors.append(f"{nav} does not look like a Fern version nav file")
151235

236+
errors.extend(check_latest_display_name(root))
237+
errors.extend(check_as_of_versions(root))
152238
return errors
153239

154240

fern/versions/v0.5.8/pages/plugins/example.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ description: ""
44
position: 2
55
---
66
<Warning>
7-
Experimental Feature
8-
The plugin system is currently **experimental** and under active development. The documentation, examples, and plugin interface are subject to significant changes in future releases. If you encounter any issues, have questions, or have ideas for improvement, please consider starting [a discussion on GitHub](https://github.com/NVIDIA-NeMo/DataDesigner/discussions).
7+
Experimental in this version
8+
Plugins were experimental in v0.5.8 and v0.5.9. For stable plugin docs, see the [v0.6.0 plugin docs](/v0.6.0/plugins/overview).
99
</Warning>
1010

1111
# Example Plugin: Column Generator

fern/versions/v0.5.8/pages/plugins/filesystem_seed_reader.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ description: ""
44
position: 3
55
---
66
<Warning>
7-
Experimental Feature
8-
The plugin system is currently **experimental** and under active development. The documentation, examples, and plugin interface are subject to significant changes in future releases. If you encounter any issues, have questions, or have ideas for improvement, please consider starting [a discussion on GitHub](https://github.com/NVIDIA-NeMo/DataDesigner/discussions).
7+
Experimental in this version
8+
Plugins were experimental in v0.5.8 and v0.5.9. For stable plugin docs, see the [v0.6.0 plugin docs](/v0.6.0/plugins/overview).
99
</Warning>
1010

1111
`FileSystemSeedReader` is the simplest way to build a seed reader plugin when your source data lives in a directory of files. You describe the files cheaply in `build_manifest(...)`, then optionally read and reshape them in `hydrate_row(...)`.

fern/versions/v0.5.8/pages/plugins/overview.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ description: ""
44
position: 1
55
---
66
<Warning>
7-
Experimental Feature
8-
The plugin system is currently **experimental** and under active development. The documentation, examples, and plugin interface are subject to significant changes in future releases. If you encounter any issues, have questions, or have ideas for improvement, please consider starting [a discussion on GitHub](https://github.com/NVIDIA-NeMo/DataDesigner/discussions).
7+
Experimental in this version
8+
Plugins were experimental in v0.5.8 and v0.5.9. For stable plugin docs, see the [v0.6.0 plugin docs](/v0.6.0/plugins/overview).
99
</Warning>
1010

1111
## What are plugins?

0 commit comments

Comments
 (0)