Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,7 @@
"v1.15.0/en/enterprise/guides/update-crew",
"v1.15.0/en/enterprise/guides/enable-crew-studio",
"v1.15.0/en/enterprise/guides/capture_telemetry_logs",
"v1.15.0/en/enterprise/guides/datadog",
"v1.15.0/en/enterprise/guides/azure-openai-setup",
"v1.15.0/en/enterprise/guides/vertex-ai-workload-identity-setup",
"v1.15.0/en/enterprise/guides/tool-repository",
Expand Down Expand Up @@ -9705,6 +9706,7 @@
"v1.15.0/pt-BR/enterprise/guides/update-crew",
"v1.15.0/pt-BR/enterprise/guides/enable-crew-studio",
"v1.15.0/pt-BR/enterprise/guides/capture_telemetry_logs",
"v1.15.0/pt-BR/enterprise/guides/datadog",
"v1.15.0/pt-BR/enterprise/guides/azure-openai-setup",
"v1.15.0/pt-BR/enterprise/guides/tool-repository",
"v1.15.0/pt-BR/enterprise/guides/custom-mcp-server",
Expand Down Expand Up @@ -18100,6 +18102,7 @@
"v1.15.0/ko/enterprise/guides/update-crew",
"v1.15.0/ko/enterprise/guides/enable-crew-studio",
"v1.15.0/ko/enterprise/guides/capture_telemetry_logs",
"v1.15.0/ko/enterprise/guides/datadog",
"v1.15.0/ko/enterprise/guides/azure-openai-setup",
"v1.15.0/ko/enterprise/guides/tool-repository",
"v1.15.0/ko/enterprise/guides/custom-mcp-server",
Expand Down Expand Up @@ -26687,6 +26690,7 @@
"v1.15.0/ar/enterprise/guides/update-crew",
"v1.15.0/ar/enterprise/guides/enable-crew-studio",
"v1.15.0/ar/enterprise/guides/capture_telemetry_logs",
"v1.15.0/ar/enterprise/guides/datadog",
"v1.15.0/ar/enterprise/guides/azure-openai-setup",
"v1.15.0/ar/enterprise/guides/tool-repository",
"v1.15.0/ar/enterprise/guides/custom-mcp-server",
Expand Down
53 changes: 25 additions & 28 deletions lib/devtools/src/crewai_devtools/docs_versioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,49 +201,46 @@ def _walk_pages(node: Any, transform: Callable[[str], str]) -> Any:
return node


def _is_version_slug(value: str) -> bool:
return bool(VERSION_SLUG_RE.match(value))


def _previous_default(versions: list[dict[str, Any]]) -> dict[str, Any] | None:
"""Return the entry currently marked default (or the first versioned)."""
def _edge_entry(versions: list[dict[str, Any]]) -> dict[str, Any] | None:
"""Return the Edge version entry, the rolling channel matching main HEAD."""
for v in versions:
if v.get("default") and _is_version_slug(v.get("version", "")):
return v
for v in versions:
if _is_version_slug(v.get("version", "")):
if v.get("version") == EDGE_VERSION:
return v
return None


def _build_new_entry(
previous: dict[str, Any], version_slug: str, locale: str, docs_root: Path
edge: dict[str, Any], version_slug: str, docs_root: Path
) -> dict[str, Any] | None:
"""Clone the previous default's nav into a new entry for ``version_slug``.
"""Clone the Edge nav into a new entry for ``version_slug``.

Freezing a release means promoting *Edge* (which tracks main HEAD) to the
new Latest, so the new version's navigation is cloned from the Edge entry
rather than from the previous frozen version. Cloning from the previous
version would silently drop every page that landed in Edge since the last
release (the file gets copied into the snapshot by ``_copy_snapshot`` but
never appears in the version selector) and would ignore any Edge nav
restructuring.

Page paths are rewritten from ``v<prev>/<locale>/...`` to
Page paths are rewritten from ``edge/<locale>/...`` to
``v<new>/<locale>/...``. Paths that don't resolve to a file in the
snapshot are pruned and the now-empty groups/tabs cascade away. Returns
``None`` if the locale has no resolvable content under the snapshot (e.g.
a locale that wasn't present in Edge yet).
freshly-copied snapshot are pruned and the now-empty groups/tabs cascade
away. Returns ``None`` if Edge has no resolvable content under the
snapshot.
"""
new_entry = copy.deepcopy(previous)
new_entry = copy.deepcopy(edge)
new_entry["version"] = version_slug
new_entry["default"] = True
new_entry["tag"] = LATEST_TAG

old_prefix = re.compile(rf"^{re.escape(previous['version'])}/")
locale_prefix = f"{locale}/"
edge_prefix = f"{EDGE_PREFIX}/"
new_prefix = f"{version_slug}/"

def transform(page: str) -> str:
if page.startswith(new_prefix):
return page
rewritten = old_prefix.sub(new_prefix, page)
if rewritten != page:
return rewritten
if page.startswith(locale_prefix):
return f"{new_prefix}{page}"
if page.startswith(edge_prefix):
return f"{new_prefix}{page[len(edge_prefix) :]}"
return page

rewritten = _walk_pages(new_entry, transform)
Expand Down Expand Up @@ -389,18 +386,18 @@ def _migrate_docs_json(docs_json: Path, version_slug: str) -> tuple[int, int, in
inserted = 0
skipped = 0
for block in data["navigation"]["languages"]:
locale = block["language"]
versions: list[dict[str, Any]] = block.get("versions", [])
if any(v.get("version") == version_slug for v in versions):
skipped += 1
continue

previous = _previous_default(versions)
if previous is None:
edge = _edge_entry(versions)
if edge is None:
# No Edge channel for this locale; nothing to freeze.
skipped += 1
continue

new_entry = _build_new_entry(previous, version_slug, locale, docs_root)
new_entry = _build_new_entry(edge, version_slug, docs_root)
if new_entry is None:
# Locale has no resolvable content under the snapshot yet (e.g. a
# locale that didn't exist in Edge). Leave the block untouched.
Expand Down
24 changes: 24 additions & 0 deletions lib/devtools/tests/test_docs_versioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ def _build_docs_root(tmp_path: Path) -> Path:
(edge_en / "api.mdx").write_text(
'---\nopenapi: "/enterprise-api.en.yaml GET /foo"\n---\n'
)
# A page added to Edge after the previous release. It exists as a file and
# is wired into the Edge nav, but is intentionally absent from the v1.14.7
# nav below — the freeze must still surface it in the new version.
(edge_en / "datadog.mdx").write_text("# Datadog (Edge)\n")
(docs / "edge" / "enterprise-api.en.yaml").write_text("openapi: 3.0.0\n")

# A pre-existing frozen snapshot to clone the nav structure from.
Expand Down Expand Up @@ -58,6 +62,7 @@ def _build_docs_root(tmp_path: Path) -> Path:
"edge/en/introduction",
"edge/en/changelog",
"edge/en/api",
"edge/en/datadog",
],
}
],
Expand Down Expand Up @@ -146,6 +151,25 @@ def test_inserts_version_after_edge_and_demotes_previous_default(
assert "default" not in previous
assert previous.get("tag") != "Latest"

def test_new_version_nav_is_cloned_from_edge_not_previous(
self, tmp_path: Path
) -> None:
# Regression: the new version's nav must come from Edge so pages added
# to Edge since the last release ship in the freeze. Cloning the
# previous version's nav silently dropped them (the file was copied
# into the snapshot but never linked in the version selector).
docs = _build_docs_root(tmp_path)

freeze("1.15.0", docs)

data = json.loads((docs / "docs.json").read_text())
versions = data["navigation"]["languages"][0]["versions"]
new_entry = next(v for v in versions if v["version"] == "v1.15.0")
pages = [p for tab in new_entry["tabs"] for p in tab["pages"]]
assert "v1.15.0/en/datadog" in pages
# And the file is present in the snapshot it points at.
assert (docs / "v1.15.0" / "en" / "datadog.mdx").is_file()

def test_updates_canonical_url_redirect_to_new_default(
self, tmp_path: Path
) -> None:
Expand Down
Loading