426e9e9c8f
# Conflicts: # ai/examples/README.md # core/src/main/java/com/netflix/conductor/core/config/ConductorProperties.java # core/src/main/java/com/netflix/conductor/core/execution/tasks/ExecutionConfig.java # core/src/test/java/com/netflix/conductor/core/execution/tasks/TestSystemTaskWorker.java # design/backfill-method-removal-architecture.md # design/file-storage-proxy-transfer.md # docs/devguide/ai/conductor-agents.md
158 lines
5.0 KiB
Python
158 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Deterministic checks for Workflows documentation structure and fixtures."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
DOCS = ROOT / "docs"
|
|
LOCAL_LINK = re.compile(r"\[[^\]]+\]\((?!https?://|mailto:|#)([^) >]+)(?:#[^)]+)?\)")
|
|
|
|
|
|
class MkDocsLoader(yaml.SafeLoader):
|
|
pass
|
|
|
|
|
|
MkDocsLoader.add_constructor(
|
|
"!relative", lambda loader, node: loader.construct_scalar(node)
|
|
)
|
|
MkDocsLoader.add_multi_constructor(
|
|
"tag:yaml.org,2002:python/name:",
|
|
lambda loader, suffix, node: suffix,
|
|
)
|
|
|
|
|
|
def load_config() -> dict[str, object]:
|
|
return yaml.load(
|
|
(ROOT / "mkdocs.yml").read_text(encoding="utf-8"), Loader=MkDocsLoader
|
|
)
|
|
|
|
|
|
def workflow_targets(nav: list[object]) -> list[str]:
|
|
return section_targets(nav, "Workflows")
|
|
|
|
|
|
def section_targets(nav: list[object], section_name: str) -> list[str]:
|
|
section = next(
|
|
(
|
|
item[section_name]
|
|
for item in nav
|
|
if isinstance(item, dict) and section_name in item
|
|
),
|
|
None,
|
|
)
|
|
if section is None:
|
|
raise AssertionError(f"missing {section_name} nav section")
|
|
targets: list[str] = []
|
|
|
|
def walk(value: object) -> None:
|
|
if isinstance(value, str):
|
|
targets.append(value)
|
|
elif isinstance(value, list):
|
|
for item in value:
|
|
walk(item)
|
|
elif isinstance(value, dict):
|
|
for child in value.values():
|
|
walk(child)
|
|
|
|
walk(section)
|
|
return targets
|
|
|
|
|
|
def check_nav() -> None:
|
|
config = load_config()
|
|
targets = workflow_targets(config["nav"])
|
|
duplicates = sorted({target for target in targets if targets.count(target) > 1})
|
|
if duplicates:
|
|
raise AssertionError(f"duplicate targets inside Workflows nav: {duplicates}")
|
|
missing = [target for target in targets if not (DOCS / target).is_file()]
|
|
if missing:
|
|
raise AssertionError(f"missing Workflows nav targets: {missing}")
|
|
|
|
|
|
def check_cookbook_nav() -> None:
|
|
config = load_config()
|
|
targets = section_targets(config["nav"], "Design Patterns")
|
|
expected = [
|
|
"devguide/cookbook/index.md",
|
|
"devguide/cookbook/microservice-orchestration.md",
|
|
"devguide/cookbook/dynamic-parallelism.md",
|
|
"devguide/cookbook/wait-and-timers.md",
|
|
"devguide/cookbook/task-timeouts-and-retries.md",
|
|
"devguide/cookbook/saga-compensation.md",
|
|
"devguide/cookbook/http-poll-long-running-job.md",
|
|
"devguide/cookbook/workflow-scheduling.md",
|
|
"devguide/cookbook/dynamic-workflows.md",
|
|
"devguide/cookbook/event-driven.md",
|
|
]
|
|
duplicates = sorted({target for target in targets if targets.count(target) > 1})
|
|
if duplicates:
|
|
raise AssertionError(f"duplicate targets inside Design Patterns nav: {duplicates}")
|
|
missing = [target for target in targets if not (DOCS / target).is_file()]
|
|
if missing:
|
|
raise AssertionError(f"missing Design Patterns nav targets: {missing}")
|
|
classic_targets = [target for target in targets if target.startswith("devguide/cookbook/")]
|
|
if classic_targets != expected:
|
|
raise AssertionError(
|
|
"Design Patterns nav targets do not match canonical recipe order: "
|
|
f"expected {expected}, got {classic_targets}"
|
|
)
|
|
|
|
|
|
def check_compatibility_routes() -> None:
|
|
required = [
|
|
DOCS / "quickstart/index.md",
|
|
DOCS / "quickstart/first-workflow.md",
|
|
DOCS / "devguide/how-tos/event-bus.md",
|
|
]
|
|
missing = [str(path.relative_to(ROOT)) for path in required if not path.is_file()]
|
|
if missing:
|
|
raise AssertionError(f"missing compatibility route(s): {missing}")
|
|
|
|
|
|
def check_local_links() -> None:
|
|
failures: list[str] = []
|
|
targets = set(workflow_targets(load_config()["nav"]))
|
|
targets.update({"quickstart/index.md"})
|
|
for relative in targets:
|
|
page = DOCS / relative
|
|
for match in LOCAL_LINK.finditer(page.read_text(encoding="utf-8")):
|
|
raw = match.group(1)
|
|
raw = raw.split("#", 1)[0]
|
|
if raw.startswith("{{") or raw.startswith("/"):
|
|
continue
|
|
target = (page.parent / raw).resolve()
|
|
if target.suffix == "":
|
|
continue
|
|
if not target.exists():
|
|
failures.append(f"{page.relative_to(ROOT)} -> {raw}")
|
|
if failures:
|
|
raise AssertionError("broken local links:\n" + "\n".join(failures))
|
|
|
|
|
|
def check_json_fixtures() -> None:
|
|
fixtures = list((ROOT / "scheduler/examples").glob("*.json"))
|
|
fixtures += list((DOCS / "devguide/cookbook/examples/events").glob("*.json"))
|
|
fixtures += [DOCS / "devguide/cookbook/examples/workflow-test.json"]
|
|
for fixture in fixtures:
|
|
json.loads(fixture.read_text(encoding="utf-8"))
|
|
|
|
|
|
def main() -> None:
|
|
check_nav()
|
|
check_cookbook_nav()
|
|
check_compatibility_routes()
|
|
check_local_links()
|
|
check_json_fixtures()
|
|
print("Workflows and Cookbook documentation checks passed")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|