Unverified Commit 9c71f286 authored by Nate Mailhot's avatar Nate Mailhot Committed by GitHub
Browse files

fix: link checking (#5209)

parent ce570d31
...@@ -289,6 +289,15 @@ def find_markdown_files(root_dir: str, logger: logging.Logger) -> List[Path]: ...@@ -289,6 +289,15 @@ def find_markdown_files(root_dir: str, logger: logging.Logger) -> List[Path]:
logger.error(error_msg) logger.error(error_msg)
raise ValueError(error_msg) raise ValueError(error_msg)
# Patterns for files to skip (attribution files, etc.)
skip_patterns = [
"ATTRIBUTION",
"ATTRIBUTIONS",
"THIRD_PARTY",
"THIRD-PARTY",
"LICENSES",
]
md_files = [] md_files = []
if root_path.is_file(): if root_path.is_file():
...@@ -302,6 +311,11 @@ def find_markdown_files(root_dir: str, logger: logging.Logger) -> List[Path]: ...@@ -302,6 +311,11 @@ def find_markdown_files(root_dir: str, logger: logging.Logger) -> List[Path]:
# If it's a directory, find all .md files recursively # If it's a directory, find all .md files recursively
logger.debug(f"Scanning directory recursively: {root_path}") logger.debug(f"Scanning directory recursively: {root_path}")
for file_path in root_path.rglob("*.md"): for file_path in root_path.rglob("*.md"):
# Skip attribution files
file_name_upper = file_path.name.upper()
if any(pattern in file_name_upper for pattern in skip_patterns):
logger.debug(f"Skipping attribution file: {file_path}")
continue
md_files.append(file_path) md_files.append(file_path)
logger.info(f"Found {len(md_files)} markdown files in {root_dir}") logger.info(f"Found {len(md_files)} markdown files in {root_dir}")
...@@ -400,15 +414,15 @@ def resolve_link_path( ...@@ -400,15 +414,15 @@ def resolve_link_path(
logger.debug(f"Absolute filesystem path detected: {link_url}") logger.debug(f"Absolute filesystem path detected: {link_url}")
return Path(link_url) return Path(link_url)
# Handle symlinks: resolve relative paths from the target's directory, not the symlink's directory # For symlinks, resolve relative paths from the symlink's location, not the
# target's location. This matches GitHub's behavior where links in symlinked
# files are resolved relative to the symlink
source_dir = source_file.parent
if source_file.is_symlink(): if source_file.is_symlink():
# Get the real path (target) of the symlink logger.debug(
real_source_file = source_file.resolve() f"Source file is a symlink, resolving links from symlink location: "
source_dir = real_source_file.parent f"{source_dir}"
logger.debug(f"Source file is a symlink, using target directory: {source_dir}") )
else:
# Resolve relative path from the source file's directory
source_dir = source_file.parent
resolved_path = source_dir / link_url resolved_path = source_dir / link_url
...@@ -472,42 +486,96 @@ def validate_links( ...@@ -472,42 +486,96 @@ def validate_links(
link_url, md_file, logger, git_root_dir link_url, md_file, logger, git_root_dir
) )
# Only check if the target should be a markdown file # Determine if this is a markdown file, directory, or should be skipped
if is_markdown_file(resolved_path): is_markdown = is_markdown_file(resolved_path)
is_directory_link = link_url.endswith("/") or (
resolved_path.exists() and resolved_path.is_dir()
)
# Check if link has a non-markdown file extension (image, code file, etc.)
has_other_extension = (
resolved_path.suffix
and resolved_path.suffix.lower() not in [".md", ""]
)
# Skip non-markdown files (images, videos, code files, etc.)
# but validate markdown files and directory links (with or without trailing slash)
if not is_markdown and not is_directory_link and has_other_extension:
logger.debug(
f"Skipping non-markdown file link in {md_file}:{line_num} - {link_url}"
)
continue
# Validate the link
is_broken = False
error_reason = None
if is_markdown:
# Check markdown file exists
if not resolved_path.exists(): if not resolved_path.exists():
# Generate GitHub URL for the broken link line is_broken = True
file_for_github = ( error_reason = f"Markdown file does not exist: {resolved_path}"
path_relative_to_git_root(md_file, git_root_dir, logger) else:
if git_root_dir logger.debug(
else str(md_file) f"Valid markdown link in {md_file}:{line_num} - {link_url} -> {resolved_path}"
)
github_url = (
construct_github_url(
file_for_github, git_info, logger, line_num
)
if git_info
else ""
) )
elif is_directory_link:
broken_link_info = { # It's a directory link (ends with / or resolves to existing directory)
"line": line_num, # Check if directory exists
"link_text": link_text, if not resolved_path.exists():
"link_url": link_url, is_broken = True
"resolved_path": str(resolved_path), error_reason = f"Directory does not exist: {resolved_path}"
"github_url": github_url, elif not resolved_path.is_dir():
} is_broken = True
broken_links.append(broken_link_info) error_reason = (
total_broken_links += 1 f"Path exists but is not a directory: {resolved_path}"
logger.warning(
f"Broken link found in {md_file}:{line_num} - {link_url} -> {resolved_path}"
) )
else: else:
logger.debug( logger.debug(
f"Valid link in {md_file}:{line_num} - {link_url} -> {resolved_path}" f"Valid directory link in {md_file}:{line_num} - {link_url} -> {resolved_path}"
) )
else: else:
logger.debug( # Link without extension that's not markdown or directory
f"Skipping non-markdown link in {md_file}:{line_num} - {link_url}" # Could be LICENSE, Makefile, etc. - check if it exists
if not resolved_path.exists():
is_broken = True
error_reason = (
f"File does not exist: {resolved_path}. "
f"If this is a directory link, add a trailing slash (/)"
)
else:
logger.debug(
f"Valid file link in {md_file}:{line_num} - {link_url} -> {resolved_path}"
)
# Report broken link if found
if is_broken:
# Generate GitHub URL for the broken link line
file_for_github = (
path_relative_to_git_root(md_file, git_root_dir, logger)
if git_root_dir
else str(md_file)
)
github_url = (
construct_github_url(
file_for_github, git_info, logger, line_num
)
if git_info
else ""
)
broken_link_info = {
"line": line_num,
"link_text": link_text,
"link_url": link_url,
"resolved_path": str(resolved_path),
"error_reason": error_reason,
"github_url": github_url,
}
broken_links.append(broken_link_info)
total_broken_links += 1
logger.warning(
f"Broken link found in {md_file}:{line_num} - {link_url} -> {error_reason}"
) )
except Exception as e: except Exception as e:
...@@ -528,7 +596,8 @@ def validate_links( ...@@ -528,7 +596,8 @@ def validate_links(
"line": line_num, "line": line_num,
"link_text": link_text, "link_text": link_text,
"link_url": link_url, "link_url": link_url,
"resolved_path": f"ERROR: {e}", "resolved_path": "ERROR",
"error_reason": f"Error resolving link: {e}",
"github_url": github_url, "github_url": github_url,
} }
broken_links.append(broken_link_info) broken_links.append(broken_link_info)
......
...@@ -168,16 +168,18 @@ jobs: ...@@ -168,16 +168,18 @@ jobs:
line = link['line'] line = link['line']
link_text = link['link_text'] link_text = link['link_text']
link_url = link['link_url'] link_url = link['link_url']
error_reason = link.get('error_reason', 'Link target not found')
github_url = link.get('github_url', '') github_url = link.get('github_url', '')
# Create GitHub annotation for each broken link # Create GitHub annotation for each broken link
annotation_msg = f'Broken link: [{link_text}]({link_url})' annotation_msg = f'Broken link: [{link_text}]({link_url}) - {error_reason}'
if github_url: if github_url:
annotation_msg += f' - View: {github_url}' annotation_msg += f' - View: {github_url}'
print(f'::error file={file_path},line={line}::{annotation_msg}') print(f'::error file={file_path},line={line}::{annotation_msg}')
# Display in workflow output # Display in workflow output
print(f' {i}. Line {line}: [{link_text}]({link_url})') print(f' {i}. Line {line}: [{link_text}]({link_url})')
print(f' ❌ {error_reason}')
if github_url: if github_url:
print(f' 🔗 View on GitHub: {github_url}') print(f' 🔗 View on GitHub: {github_url}')
else: else:
......
...@@ -195,9 +195,9 @@ Pre-built deployment configurations for common models and topologies: ...@@ -195,9 +195,9 @@ Pre-built deployment configurations for common models and topologies:
| Model | Framework | Mode | GPUs | Recipe | | Model | Framework | Mode | GPUs | Recipe |
|-------|-----------|------|------|--------| |-------|-----------|------|------|--------|
| Llama-3.1-70B | vLLM | Aggregated | 4x H100 | [View](recipes/vllm/llama-3.1-70b/) | | Llama-3-70B | vLLM | Aggregated | 4x H100 | [View](recipes/llama-3-70b/vllm/) |
| DeepSeek-R1 | SGLang | Disaggregated | 8x H200 | [View](recipes/sglang/deepseek-r1/) | | DeepSeek-R1 | SGLang | Disaggregated | 8x H200 | [View](recipes/deepseek-r1/sglang/) |
| Qwen3-32B | TensorRT-LLM | Disaggregated | 8x GPU | [View](recipes/trtllm/qwen3-32b/) | | Qwen3-32B-FP8 | TensorRT-LLM | Aggregated | 8x GPU | [View](recipes/qwen3-32b-fp8/trtllm/) |
See [recipes/README.md](recipes/README.md) for the full list and deployment instructions. See [recipes/README.md](recipes/README.md) for the full list and deployment instructions.
......
...@@ -34,19 +34,19 @@ Learn fundamental Dynamo concepts through these introductory examples: ...@@ -34,19 +34,19 @@ Learn fundamental Dynamo concepts through these introductory examples:
These examples show how Dynamo broadly works using major inference engines. These examples show how Dynamo broadly works using major inference engines.
If you want to see advanced, framework-specific deployment patterns and best practices, check out the [Examples Backends](../examples/backends/) directory: If you want to see advanced, framework-specific deployment patterns and best practices, check out the [Examples Backends](/examples/backends/) directory:
- **[vLLM](backends/vllm/)** – vLLM-specific deployment and configuration - **[vLLM](/examples/backends/vllm/)** – vLLM-specific deployment and configuration
- **[SGLang](backends/sglang/)** – SGLang integration examples and workflows - **[SGLang](/examples/backends/sglang/)** – SGLang integration examples and workflows
- **[TensorRT-LLM](backends/trtllm/)** – TensorRT-LLM workflows and optimizations - **[TensorRT-LLM](/examples/backends/trtllm/)** – TensorRT-LLM workflows and optimizations
## Deployment Examples ## Deployment Examples
Platform-specific deployment guides for production environments: Platform-specific deployment guides for production environments:
- **[Amazon EKS](deployments/EKS/)** - Deploy Dynamo on Amazon Elastic Kubernetes Service - **[Amazon EKS](/examples/deployments/EKS/)** - Deploy Dynamo on Amazon Elastic Kubernetes Service
- **[Azure AKS](deployments/AKS/)** - Deploy Dynamo on Azure Kubernetes Service - **[Azure AKS](/examples/deployments/AKS/)** - Deploy Dynamo on Azure Kubernetes Service
- **[Amazon ECS](deployments/ECS/)** - Deploy Dynamo on Amazon Elastic Container Service - **[Amazon ECS](/examples/deployments/ECS/)** - Deploy Dynamo on Amazon Elastic Container Service
- **[Router Standalone](deployments/router_standalone/)** - Standalone router deployment patterns - **[Router Standalone](/examples/deployments/router_standalone/)** - Standalone router deployment patterns
- **Google GKE** - _Coming soon_ - **Google GKE** - _Coming soon_
## Runtime Examples ## Runtime Examples
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment