mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-03 16:11:17 +08:00
* fix(docs): sync redirects with versioned snapshots Signed-off-by: Johnny Greco <jogreco@nvidia.com> * fix(docs): make redirect validation types explicit Signed-off-by: Johnny Greco <jogreco@nvidia.com> --------- Signed-off-by: Johnny Greco <jogreco@nvidia.com>
692 lines
23 KiB
Python
692 lines
23 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
# /// script
|
|
# requires-python = ">=3.9"
|
|
# dependencies = [
|
|
# "packaging==25.0",
|
|
# "PyYAML==6.0.2",
|
|
# ]
|
|
# ///
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import shutil
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import cast
|
|
from urllib.parse import urlsplit
|
|
|
|
import yaml
|
|
from packaging.version import InvalidVersion, Version
|
|
|
|
SLUG_RE = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
DISPLAY_VERSION_RE = re.compile(r"\bv?(\d+\.\d+\.\d+(?:[.-]?[A-Za-z0-9]+)*)\b")
|
|
VERSION_AVAILABILITIES = {"beta", "deprecated", "ga", "stable"}
|
|
SNAPSHOT_METADATA_FILE = ".docs-snapshots.yml"
|
|
YamlMapping = dict[str, object]
|
|
|
|
|
|
@dataclass
|
|
class VersionEntry:
|
|
slug: str
|
|
display_name: str
|
|
path: str
|
|
availability: str | None = None
|
|
announcement: YamlMapping | None = None
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Sync or remove docs snapshots in the docs-website branch."
|
|
)
|
|
parser.add_argument("--operation", choices=["sync", "remove"], default="sync")
|
|
parser.add_argument("--source-root", type=Path)
|
|
parser.add_argument("--docs-website-root", required=True, type=Path)
|
|
parser.add_argument(
|
|
"--channel", required=True, choices=["dev", "latest", "stable", "version"]
|
|
)
|
|
parser.add_argument("--source-ref", default="")
|
|
parser.add_argument("--source-sha", default="")
|
|
parser.add_argument("--release-version", default="")
|
|
parser.add_argument("--version-slug", default="")
|
|
parser.add_argument("--display-name", default="")
|
|
parser.add_argument("--availability", default="")
|
|
parser.add_argument("--allow-rollback", action="store_true")
|
|
return parser.parse_args()
|
|
|
|
|
|
def clean_input(value: str | None) -> str:
|
|
return (value or "").strip()
|
|
|
|
|
|
def resolve_slug(channel: str, version_slug: str) -> str:
|
|
if channel == "dev":
|
|
return "dev"
|
|
if channel == "latest":
|
|
return "latest"
|
|
if not version_slug:
|
|
raise ValueError(
|
|
"--version-slug is required when --channel=stable or --channel=version"
|
|
)
|
|
if not SLUG_RE.fullmatch(version_slug):
|
|
raise ValueError(
|
|
f"version slug contains unsupported characters: {version_slug}"
|
|
)
|
|
return version_slug
|
|
|
|
|
|
def resolve_display_name(
|
|
channel: str, slug: str, source_ref: str, override: str
|
|
) -> str:
|
|
if override:
|
|
return override
|
|
if channel == "dev":
|
|
return "dev"
|
|
if channel == "latest":
|
|
return f"Latest ({source_ref})" if source_ref.startswith("v") else "Latest"
|
|
return slug
|
|
|
|
|
|
def resolve_availability(override: str) -> str | None:
|
|
availability = override
|
|
if not availability:
|
|
return None
|
|
if availability not in VERSION_AVAILABILITIES:
|
|
supported = ", ".join(sorted(VERSION_AVAILABILITIES))
|
|
raise ValueError(
|
|
f"unsupported version availability {availability!r}; expected one of: {supported}"
|
|
)
|
|
return availability
|
|
|
|
|
|
def parse_release_version(value: str) -> Version:
|
|
try:
|
|
return Version(value.removeprefix("v"))
|
|
except InvalidVersion as exc:
|
|
raise ValueError(f"invalid release version: {value}") from exc
|
|
|
|
|
|
def ensure_existing(path: Path, label: str) -> None:
|
|
if not path.exists():
|
|
raise FileNotFoundError(f"{label} does not exist: {path}")
|
|
|
|
|
|
def reset_directory(src: Path, dst: Path) -> None:
|
|
ensure_existing(src, "source directory")
|
|
if dst.exists():
|
|
shutil.rmtree(dst)
|
|
shutil.copytree(src, dst)
|
|
|
|
|
|
def merge_directory(src: Path, dst: Path, *, overwrite: bool) -> None:
|
|
if not src.exists():
|
|
return
|
|
if overwrite:
|
|
shutil.copytree(src, dst, dirs_exist_ok=True)
|
|
return
|
|
for copied in src.rglob("*"):
|
|
relative = copied.relative_to(src)
|
|
target = dst / relative
|
|
if copied.is_dir():
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
continue
|
|
if target.exists():
|
|
continue
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(copied, target)
|
|
|
|
|
|
def copy_if_exists(src: Path, dst: Path) -> None:
|
|
if src.exists():
|
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(src, dst)
|
|
|
|
|
|
def read_yaml(path: Path) -> YamlMapping:
|
|
ensure_existing(path, "YAML file")
|
|
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
if not isinstance(data, dict):
|
|
raise ValueError(f"expected YAML mapping in {path}")
|
|
return cast("YamlMapping", data)
|
|
|
|
|
|
def write_yaml(path: Path, data: YamlMapping) -> None:
|
|
path.write_text(
|
|
yaml.safe_dump(data, sort_keys=False, allow_unicode=True),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def read_snapshot_metadata(path: Path) -> dict[str, dict[str, str]]:
|
|
if not path.exists():
|
|
return {}
|
|
data = read_yaml(path)
|
|
raw_snapshots = data.get("snapshots")
|
|
if raw_snapshots is None:
|
|
return {}
|
|
if not isinstance(raw_snapshots, dict):
|
|
raise ValueError(f"expected snapshots mapping in {path}")
|
|
|
|
snapshots: dict[str, dict[str, str]] = {}
|
|
for raw_slug, raw_snapshot in raw_snapshots.items():
|
|
if not isinstance(raw_slug, str) or not isinstance(raw_snapshot, dict):
|
|
raise ValueError(f"invalid snapshot metadata in {path}")
|
|
snapshot = cast("YamlMapping", raw_snapshot)
|
|
source_ref = snapshot.get("source-ref")
|
|
source_sha = snapshot.get("source-sha", "")
|
|
version = snapshot.get("version")
|
|
if (
|
|
not isinstance(source_ref, str)
|
|
or not isinstance(source_sha, str)
|
|
or not isinstance(version, str)
|
|
):
|
|
raise ValueError(f"invalid snapshot metadata for {raw_slug} in {path}")
|
|
snapshots[raw_slug] = {
|
|
"source-ref": source_ref,
|
|
"source-sha": source_sha,
|
|
"version": version,
|
|
}
|
|
return snapshots
|
|
|
|
|
|
def write_snapshot_metadata(path: Path, snapshots: dict[str, dict[str, str]]) -> None:
|
|
write_yaml(path, {"snapshots": snapshots})
|
|
|
|
|
|
def seed_mutable_snapshot_metadata(
|
|
snapshots: dict[str, dict[str, str]], docs_yml: Path, slug: str
|
|
) -> None:
|
|
if slug in snapshots:
|
|
return
|
|
data = read_yaml(docs_yml)
|
|
for entry in parse_versions(data.get("versions")):
|
|
if entry.slug != slug:
|
|
continue
|
|
match = DISPLAY_VERSION_RE.search(entry.display_name)
|
|
if match is not None:
|
|
snapshots[slug] = {
|
|
"source-ref": "",
|
|
"source-sha": "",
|
|
"version": str(parse_release_version(match.group(1))),
|
|
}
|
|
return
|
|
|
|
|
|
def ensure_immutable_snapshot(
|
|
snapshots: dict[str, dict[str, str]],
|
|
target_fern: Path,
|
|
slug: str,
|
|
source_sha: str,
|
|
) -> None:
|
|
existing = snapshots.get(slug)
|
|
if existing is not None:
|
|
if existing["source-sha"] != source_sha:
|
|
raise ValueError(
|
|
f"immutable snapshot {slug} already points to "
|
|
f"{existing['source-sha']}, not {source_sha}"
|
|
)
|
|
return
|
|
if (target_fern / f"pages-{slug}").exists():
|
|
raise ValueError(
|
|
f"immutable snapshot {slug} already exists without source metadata"
|
|
)
|
|
|
|
|
|
def ensure_monotonic_snapshot(
|
|
snapshots: dict[str, dict[str, str]],
|
|
slug: str,
|
|
source_sha: str,
|
|
release_version: str,
|
|
*,
|
|
allow_rollback: bool,
|
|
) -> bool:
|
|
existing = snapshots.get(slug)
|
|
if existing is None:
|
|
return True
|
|
|
|
incoming_version = parse_release_version(release_version)
|
|
existing_version = parse_release_version(existing["version"])
|
|
if incoming_version < existing_version and not allow_rollback:
|
|
return False
|
|
if (
|
|
incoming_version == existing_version
|
|
and bool(existing["source-sha"])
|
|
and existing["source-sha"] != source_sha
|
|
and not allow_rollback
|
|
):
|
|
raise ValueError(
|
|
f"snapshot {slug} version {release_version} already points to "
|
|
f"{existing['source-sha']}, not {source_sha}"
|
|
)
|
|
return True
|
|
|
|
|
|
def prefix_path(value: object, pages_dir: str) -> object:
|
|
if not isinstance(value, str):
|
|
return value
|
|
if value.startswith(("../", "/", "http://", "https://")):
|
|
return value
|
|
return f"../{pages_dir}/{value}"
|
|
|
|
|
|
def prefix_navigation_paths(value: object, pages_dir: str) -> object:
|
|
if isinstance(value, dict):
|
|
mapping = cast("YamlMapping", value)
|
|
for key in ("path", "folder"):
|
|
if key in mapping:
|
|
mapping[key] = prefix_path(mapping[key], pages_dir)
|
|
for child in mapping.values():
|
|
prefix_navigation_paths(child, pages_dir)
|
|
elif isinstance(value, list):
|
|
for child in cast("list[object]", value):
|
|
prefix_navigation_paths(child, pages_dir)
|
|
return value
|
|
|
|
|
|
def version_navigation(source_index: Path, pages_dir: str) -> YamlMapping:
|
|
data = read_yaml(source_index)
|
|
prefix_navigation_paths(data, pages_dir)
|
|
return data
|
|
|
|
|
|
def parse_versions(raw_versions: object) -> list[VersionEntry]:
|
|
if raw_versions is None:
|
|
return []
|
|
if not isinstance(raw_versions, list):
|
|
raise ValueError("docs.yml versions must be a list")
|
|
entries: list[VersionEntry] = []
|
|
for raw in cast("list[object]", raw_versions):
|
|
if not isinstance(raw, dict):
|
|
continue
|
|
entry = cast("YamlMapping", raw)
|
|
slug = entry.get("slug")
|
|
display_name = entry.get("display-name")
|
|
path = entry.get("path")
|
|
availability = entry.get("availability")
|
|
announcement = entry.get("announcement")
|
|
if (
|
|
isinstance(slug, str)
|
|
and isinstance(display_name, str)
|
|
and isinstance(path, str)
|
|
):
|
|
entries.append(
|
|
VersionEntry(
|
|
slug=slug,
|
|
display_name=display_name,
|
|
path=path,
|
|
availability=availability
|
|
if isinstance(availability, str)
|
|
else None,
|
|
announcement=cast("YamlMapping", announcement)
|
|
if isinstance(announcement, dict)
|
|
else None,
|
|
)
|
|
)
|
|
return entries
|
|
|
|
|
|
def ordered_entries(
|
|
existing: list[VersionEntry], updated: VersionEntry
|
|
) -> list[VersionEntry]:
|
|
by_slug = {entry.slug: entry for entry in existing}
|
|
by_slug[updated.slug] = updated
|
|
pinned = [by_slug[slug] for slug in ("latest", "dev") if slug in by_slug]
|
|
|
|
versioned: list[tuple[Version, VersionEntry]] = []
|
|
other: list[VersionEntry] = []
|
|
for entry in by_slug.values():
|
|
if entry.slug in {"latest", "dev"}:
|
|
continue
|
|
try:
|
|
versioned.append((parse_release_version(entry.slug), entry))
|
|
except ValueError:
|
|
other.append(entry)
|
|
|
|
versioned.sort(key=lambda item: item[0], reverse=True)
|
|
return pinned + [entry for _, entry in versioned] + other
|
|
|
|
|
|
def render_versions(entries: list[VersionEntry]) -> list[YamlMapping]:
|
|
rendered: list[YamlMapping] = []
|
|
for entry in entries:
|
|
item = {
|
|
"display-name": entry.display_name,
|
|
"path": entry.path,
|
|
"slug": entry.slug,
|
|
}
|
|
if entry.availability is not None:
|
|
item["availability"] = entry.availability
|
|
if entry.announcement is not None:
|
|
item["announcement"] = entry.announcement
|
|
rendered.append(item)
|
|
return rendered
|
|
|
|
|
|
def sync_global_announcement(source_docs_yml: Path, target_docs_yml: Path) -> None:
|
|
source_data = read_yaml(source_docs_yml)
|
|
source_announcement = source_data.get("announcement")
|
|
if source_announcement is not None and not isinstance(source_announcement, dict):
|
|
raise ValueError("docs.yml announcement must be a mapping")
|
|
|
|
target_data = read_yaml(target_docs_yml)
|
|
if source_announcement is None:
|
|
target_data.pop("announcement", None)
|
|
else:
|
|
target_data["announcement"] = source_announcement
|
|
write_yaml(target_docs_yml, target_data)
|
|
|
|
|
|
def sync_redirects(source_docs_yml: Path, target_docs_yml: Path, slug: str) -> None:
|
|
"""Refresh routing alongside its mutable snapshot, including deleted rules."""
|
|
source_data = read_yaml(source_docs_yml)
|
|
target_data = read_yaml(target_docs_yml)
|
|
version_slugs = {"dev", "latest"} | {
|
|
entry.slug
|
|
for data in (source_data, target_data)
|
|
for entry in parse_versions(data.get("versions"))
|
|
}
|
|
|
|
def redirects(data: YamlMapping) -> list[YamlMapping]:
|
|
value = data.get("redirects", [])
|
|
rules = cast("list[YamlMapping]", value)
|
|
if not isinstance(value, list) or any(
|
|
not isinstance(rule, dict)
|
|
or not isinstance(rule.get("source"), str)
|
|
or not isinstance(rule.get("destination"), str)
|
|
for rule in rules
|
|
):
|
|
raise ValueError(
|
|
"docs.yml redirects must be a list of source/destination mappings"
|
|
)
|
|
return rules
|
|
|
|
def owner(rule: YamlMapping) -> str | None:
|
|
# A versioned source owns its redirect even when it targets another
|
|
# version. Unversioned aliases belong to their destination's version.
|
|
for field in ("source", "destination"):
|
|
url = urlsplit(cast("str", rule[field]))
|
|
if not url.netloc and url.path.startswith("/openshell/"):
|
|
version = url.path.removeprefix("/openshell/").split("/", 1)[0]
|
|
if version in version_slugs:
|
|
return version
|
|
# Dev owns shared rules such as the legacy .html URL normalization.
|
|
return None
|
|
|
|
def selected(rule: YamlMapping) -> bool:
|
|
channel = owner(rule)
|
|
return channel == slug or (channel is None and slug == "dev")
|
|
|
|
retained = [rule for rule in redirects(target_data) if not selected(rule)]
|
|
updated = [rule for rule in redirects(source_data) if selected(rule)]
|
|
# Keep source ordering (explicit rules before wildcards), and place the
|
|
# refreshed channel's rules before shared fallback rules.
|
|
target_data["redirects"] = sorted(
|
|
updated + retained, key=lambda rule: owner(rule) is None
|
|
)
|
|
write_yaml(target_docs_yml, target_data)
|
|
|
|
|
|
def source_version_announcement(docs_yml: Path, slug: str) -> YamlMapping | None:
|
|
entries = parse_versions(read_yaml(docs_yml).get("versions"))
|
|
for entry in entries:
|
|
if entry.slug == slug:
|
|
return entry.announcement
|
|
if len(entries) == 1:
|
|
return entries[0].announcement
|
|
return None
|
|
|
|
|
|
def component_dirs(fern_dir: Path) -> list[str]:
|
|
dirs: list[str] = []
|
|
preferred = ["pages-latest", "pages-dev"]
|
|
all_page_dirs = sorted(
|
|
path.name for path in fern_dir.glob("pages-*") if path.is_dir()
|
|
)
|
|
for name in preferred + all_page_dirs:
|
|
path = fern_dir / name / "_components"
|
|
component = f"./{name}/_components"
|
|
if path.is_dir() and component not in dirs:
|
|
dirs.append(component)
|
|
dirs.append("./components")
|
|
return dirs
|
|
|
|
|
|
def update_docs_yml(docs_yml: Path, updated: VersionEntry, fern_dir: Path) -> None:
|
|
data = read_yaml(docs_yml)
|
|
data["experimental"] = {
|
|
"mdx-components": component_dirs(fern_dir),
|
|
}
|
|
data["versions"] = render_versions(
|
|
ordered_entries(parse_versions(data.get("versions")), updated)
|
|
)
|
|
write_yaml(docs_yml, data)
|
|
|
|
|
|
def write_snapshot(
|
|
source_docs: Path,
|
|
source_fern: Path,
|
|
target_fern: Path,
|
|
entry: VersionEntry,
|
|
*,
|
|
refresh_shared: bool,
|
|
) -> None:
|
|
pages_dir = f"pages-{entry.slug}"
|
|
reset_directory(source_docs, target_fern / pages_dir)
|
|
if refresh_shared:
|
|
merge_directory(source_fern / "assets", target_fern / "assets", overwrite=True)
|
|
merge_directory(
|
|
source_fern / "components", target_fern / "components", overwrite=True
|
|
)
|
|
copy_if_exists(source_fern / "main.css", target_fern / "main.css")
|
|
copy_if_exists(
|
|
source_fern / "fern.config.json", target_fern / "fern.config.json"
|
|
)
|
|
sync_global_announcement(source_fern / "docs.yml", target_fern / "docs.yml")
|
|
|
|
if entry.slug in {"dev", "latest"}:
|
|
sync_redirects(source_fern / "docs.yml", target_fern / "docs.yml", entry.slug)
|
|
|
|
versions_dir = target_fern / "versions"
|
|
versions_dir.mkdir(parents=True, exist_ok=True)
|
|
write_yaml(
|
|
versions_dir / f"{entry.slug}.yml",
|
|
version_navigation(source_docs / "index.yml", pages_dir),
|
|
)
|
|
update_docs_yml(target_fern / "docs.yml", entry, target_fern)
|
|
|
|
|
|
def remove_docs_yml_entry(docs_yml: Path, slug: str, fern_dir: Path) -> None:
|
|
data = read_yaml(docs_yml)
|
|
entries = [
|
|
entry for entry in parse_versions(data.get("versions")) if entry.slug != slug
|
|
]
|
|
data["experimental"] = {
|
|
"mdx-components": component_dirs(fern_dir),
|
|
}
|
|
data["versions"] = render_versions(entries)
|
|
write_yaml(docs_yml, data)
|
|
|
|
|
|
def sync_docs(args: argparse.Namespace) -> None:
|
|
if args.source_root is None:
|
|
raise ValueError("--source-root is required when --operation=sync")
|
|
source_root = args.source_root.resolve()
|
|
docs_root = args.docs_website_root.resolve()
|
|
source_docs = source_root / "docs"
|
|
source_fern = source_root / "fern"
|
|
target_fern = docs_root / "fern"
|
|
|
|
ensure_existing(source_docs, "source docs")
|
|
ensure_existing(source_fern, "source fern config")
|
|
ensure_existing(target_fern, "docs website fern directory")
|
|
|
|
channel = clean_input(args.channel)
|
|
source_ref = clean_input(args.source_ref)
|
|
if not source_ref:
|
|
raise ValueError("--source-ref is required when --operation=sync")
|
|
source_sha = clean_input(getattr(args, "source_sha", ""))
|
|
if not source_sha:
|
|
raise ValueError("--source-sha is required when --operation=sync")
|
|
release_version = clean_input(getattr(args, "release_version", ""))
|
|
version_slug = clean_input(args.version_slug)
|
|
display_override = clean_input(args.display_name)
|
|
availability_override = clean_input(args.availability)
|
|
if channel in {"dev", "latest", "stable"} and not release_version:
|
|
raise ValueError(
|
|
"--release-version is required for dev, latest, and stable channels"
|
|
)
|
|
slug = resolve_slug(channel, version_slug)
|
|
display_name = resolve_display_name(channel, slug, source_ref, display_override)
|
|
availability = resolve_availability(availability_override)
|
|
metadata_path = target_fern / SNAPSHOT_METADATA_FILE
|
|
snapshots = read_snapshot_metadata(metadata_path)
|
|
docs_yml = target_fern / "docs.yml"
|
|
|
|
if channel == "stable":
|
|
parsed_version = parse_release_version(release_version)
|
|
expected_slug = f"v{parsed_version}"
|
|
if slug != expected_slug:
|
|
raise ValueError(f"stable version slug must be {expected_slug}, got {slug}")
|
|
ensure_immutable_snapshot(snapshots, target_fern, slug, source_sha)
|
|
write_snapshot(
|
|
source_docs,
|
|
source_fern,
|
|
target_fern,
|
|
VersionEntry(
|
|
slug=slug,
|
|
display_name=slug,
|
|
path=f"./versions/{slug}.yml",
|
|
availability=availability,
|
|
announcement=source_version_announcement(
|
|
source_fern / "docs.yml", slug
|
|
),
|
|
),
|
|
refresh_shared=False,
|
|
)
|
|
snapshots[slug] = {
|
|
"source-ref": source_ref,
|
|
"source-sha": source_sha,
|
|
"version": str(parsed_version),
|
|
}
|
|
|
|
seed_mutable_snapshot_metadata(snapshots, docs_yml, "latest")
|
|
if ensure_monotonic_snapshot(
|
|
snapshots,
|
|
"latest",
|
|
source_sha,
|
|
release_version,
|
|
allow_rollback=bool(getattr(args, "allow_rollback", False)),
|
|
):
|
|
write_snapshot(
|
|
source_docs,
|
|
source_fern,
|
|
target_fern,
|
|
VersionEntry(
|
|
slug="latest",
|
|
display_name=display_override or f"Latest ({slug})",
|
|
path="./versions/latest.yml",
|
|
availability=availability,
|
|
announcement=source_version_announcement(
|
|
source_fern / "docs.yml", "latest"
|
|
),
|
|
),
|
|
refresh_shared=False,
|
|
)
|
|
snapshots["latest"] = {
|
|
"source-ref": source_ref,
|
|
"source-sha": source_sha,
|
|
"version": str(parsed_version),
|
|
}
|
|
write_snapshot_metadata(metadata_path, snapshots)
|
|
print(f"Synced immutable {slug} docs from {source_ref}")
|
|
return
|
|
|
|
if channel in {"dev", "latest"}:
|
|
seed_mutable_snapshot_metadata(snapshots, docs_yml, slug)
|
|
if not ensure_monotonic_snapshot(
|
|
snapshots,
|
|
slug,
|
|
source_sha,
|
|
release_version,
|
|
allow_rollback=bool(getattr(args, "allow_rollback", False)),
|
|
):
|
|
print(
|
|
f"Skipped stale {slug} docs {release_version}; "
|
|
f"current version is {snapshots[slug]['version']}"
|
|
)
|
|
return
|
|
else:
|
|
ensure_immutable_snapshot(snapshots, target_fern, slug, source_sha)
|
|
release_version = release_version or slug.removeprefix("v")
|
|
|
|
write_snapshot(
|
|
source_docs,
|
|
source_fern,
|
|
target_fern,
|
|
VersionEntry(
|
|
slug=slug,
|
|
display_name=display_name,
|
|
path=f"./versions/{slug}.yml",
|
|
availability=availability,
|
|
announcement=source_version_announcement(source_fern / "docs.yml", slug),
|
|
),
|
|
refresh_shared=channel == "dev",
|
|
)
|
|
snapshots[slug] = {
|
|
"source-ref": source_ref,
|
|
"source-sha": source_sha,
|
|
"version": release_version,
|
|
}
|
|
write_snapshot_metadata(metadata_path, snapshots)
|
|
|
|
print(f"Synced {channel} docs from {source_ref} to fern/pages-{slug}")
|
|
|
|
|
|
def remove_docs(args: argparse.Namespace) -> None:
|
|
docs_root = args.docs_website_root.resolve()
|
|
target_fern = docs_root / "fern"
|
|
|
|
ensure_existing(target_fern, "docs website fern directory")
|
|
|
|
channel = clean_input(args.channel)
|
|
version_slug = clean_input(args.version_slug)
|
|
slug = resolve_slug(channel, version_slug)
|
|
|
|
pages_dir = target_fern / f"pages-{slug}"
|
|
if pages_dir.exists():
|
|
shutil.rmtree(pages_dir)
|
|
|
|
version_file = target_fern / "versions" / f"{slug}.yml"
|
|
if version_file.exists():
|
|
version_file.unlink()
|
|
|
|
remove_docs_yml_entry(target_fern / "docs.yml", slug, target_fern)
|
|
metadata_path = target_fern / SNAPSHOT_METADATA_FILE
|
|
snapshots = read_snapshot_metadata(metadata_path)
|
|
if slug in snapshots:
|
|
del snapshots[slug]
|
|
write_snapshot_metadata(metadata_path, snapshots)
|
|
|
|
print(f"Removed {slug} docs from docs website branch")
|
|
|
|
|
|
def main() -> None:
|
|
try:
|
|
args = parse_args()
|
|
if args.operation == "sync":
|
|
sync_docs(args)
|
|
else:
|
|
remove_docs(args)
|
|
except Exception as exc:
|
|
print(f"error: {exc}", file=sys.stderr)
|
|
raise SystemExit(2) from exc
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|