Unverified Commit 406c4d4e authored by nv-nmailhot's avatar nv-nmailhot Committed by GitHub
Browse files

ci: add broken links check (#2927)


Signed-off-by: default avatarNate Mailhot <nmailhot@nvidia.com>
Signed-off-by: default avatarnv-nmailhot <nmailhot@nvidia.com>
parent 0e63cd9c
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""
Script to detect broken links in markdown files within a git repository.
This script:
1. Finds all .md files in the specified directory (recursively)
2. Parses each file to extract links to other .md files
3. Validates if the linked files exist
4. Generates a JSON or HTML report of broken links with line numbers
"""
import argparse
import json
import logging
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple
def setup_logging(
verbose: bool = False, log_file: Optional[str] = None
) -> logging.Logger:
"""
Set up logging configuration.
Args:
verbose: If True, set log level to DEBUG, otherwise INFO
log_file: Optional file path to write logs to
Returns:
Configured logger instance
"""
# Create logger
logger = logging.getLogger("detect_broken_links")
logger.setLevel(logging.DEBUG if verbose else logging.INFO)
# Clear any existing handlers
logger.handlers.clear()
# Create formatter
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# Console handler
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setLevel(logging.DEBUG if verbose else logging.INFO)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# File handler (if specified)
if log_file:
try:
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.info(f"Logging to file: {log_file}")
except Exception as e:
logger.error(f"Failed to set up file logging to {log_file}: {e}")
return logger
def path_relative_to_git_root(
file_path: Path, git_root_dir: str, logger: logging.Logger
) -> str:
"""
Convert a file path to be relative to the git repository root.
Args:
file_path: The file path to convert
git_root_dir: The git repository root directory
logger: Logger instance for logging
Returns:
Path relative to git repository root as string
"""
try:
git_root_abs = Path(git_root_dir).resolve()
file_abs = Path(file_path).resolve()
# Calculate relative path from git repo root to file
relative_path = file_abs.relative_to(git_root_abs)
result = str(relative_path)
logger.debug(f"Converted {file_path} to git-relative path: {result}")
return result
except ValueError as e:
# If relative_to fails, it means the file is not under the git repo
logger.warning(
f"File {file_path} is not under Git repository {git_root_dir}: {e}"
)
return str(file_path)
except Exception as e:
logger.warning(f"Error converting path {file_path} to git-relative: {e}")
return str(file_path)
def get_git_info(
logger: logging.Logger, start_path: str = "."
) -> Optional[Dict[str, str]]:
"""
Get Git repository information including remote URL and current branch.
Args:
logger: Logger instance for logging
start_path: Starting path to search for git repository
Returns:
Dictionary with 'remote_url', 'branch', and 'git_root_dir' keys, or None if not in a Git repo
"""
try:
logger.debug(f"Attempting to get Git repository information from: {start_path}")
# Find the git repository root
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True,
text=True,
cwd=start_path,
)
if result.returncode != 0:
logger.warning("Not in a Git repository or Git not available")
return None
git_root_dir = result.stdout.strip()
logger.debug(f"Found Git repository root: {git_root_dir}")
# Check if this repository has a remote origin URL
remote_result = subprocess.run(
["git", "config", "--get", "remote.origin.url"],
capture_output=True,
text=True,
cwd=git_root_dir,
)
if remote_result.returncode != 0 or not remote_result.stdout.strip():
logger.warning("Git repository has no remote origin URL")
return None
remote_url = remote_result.stdout.strip()
logger.debug(f"Remote URL: {remote_url}")
# Get current branch
branch_result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
capture_output=True,
text=True,
cwd=git_root_dir,
)
if branch_result.returncode != 0:
logger.warning("Could not get current branch")
return None
branch = branch_result.stdout.strip()
logger.debug(f"Current branch: {branch}")
# Get current commit hash
commit_result = subprocess.run(
["git", "rev-parse", "HEAD"],
capture_output=True,
text=True,
cwd=git_root_dir,
)
if commit_result.returncode != 0:
logger.warning("Could not get current commit hash")
commit_hash = "unknown"
else:
commit_hash = commit_result.stdout.strip()
logger.debug(f"Current commit: {commit_hash}")
# Get short commit hash (first 7 characters)
short_commit = commit_hash[:7] if commit_hash != "unknown" else "unknown"
return {
"remote_url": remote_url,
"branch": branch,
"commit_hash": commit_hash,
"short_commit": short_commit,
"git_root_dir": git_root_dir,
}
except Exception as e:
logger.warning(f"Error getting Git information: {e}")
return None
def construct_github_url(
file_path: str,
git_info: Dict[str, str],
logger: logging.Logger,
line_number: Optional[int] = None,
) -> str:
"""
Construct a GitHub URL for a file, optionally linking to a specific line.
Args:
file_path: Path to the file relative to the repository root
git_info: Git repository information
logger: Logger instance for logging
line_number: Optional line number to link to (adds #L{line_number} anchor)
Returns:
GitHub URL for the file, optionally with line anchor
"""
try:
remote_url = git_info["remote_url"]
branch = git_info["branch"]
# Handle SSH and HTTPS URLs
if remote_url.startswith("git@github.com:"):
# Convert SSH to HTTPS
repo_path = remote_url.replace("git@github.com:", "")
if repo_path.endswith(".git"):
repo_path = repo_path[:-4]
github_base = f"https://github.com/{repo_path}"
elif remote_url.startswith("https://github.com/"):
# Already HTTPS
github_base = remote_url
if github_base.endswith(".git"):
github_base = github_base[:-4]
else:
logger.warning(f"Unsupported remote URL format: {remote_url}")
return ""
# Handle file path - if it's already relative to git root, use as-is
# If it's absolute, convert it to be relative to the repository root
if os.path.isabs(file_path):
git_root_dir = git_info.get("git_root_dir", ".")
try:
# Get absolute paths
git_repo_abs_path = Path(git_root_dir).resolve()
file_abs_path = Path(file_path).resolve()
# Calculate relative path from git repo root to file
relative_path = file_abs_path.relative_to(git_repo_abs_path)
file_path = str(relative_path)
logger.debug(
f"Calculated relative path from Git repo root: {file_path}"
)
except ValueError as e:
# If relative_to fails, it means the file is not under the git repo
logger.warning(
f"File {file_path} is not under Git repository {git_root_dir}: {e}"
)
return ""
else:
# Path is already relative to git root, use as-is
logger.debug(f"Using git-relative path as-is: {file_path}")
# Construct the GitHub URL
github_url = f"{github_base}/blob/{branch}/{file_path}"
# Add line anchor if specified
if line_number is not None:
# For markdown files, we need to add ?plain=1 to make line anchors work
if file_path.lower().endswith(".md"):
github_url += f"?plain=1#L{line_number}"
else:
github_url += f"#L{line_number}"
logger.debug(f"Constructed GitHub URL with line anchor: {github_url}")
else:
logger.debug(f"Constructed GitHub URL: {github_url}")
return github_url
except Exception as e:
logger.warning(f"Error constructing GitHub URL: {e}")
return ""
def find_markdown_files(root_dir: str, logger: logging.Logger) -> List[Path]:
"""Find all .md files recursively in the given directory or specific file."""
root_path = Path(root_dir)
logger.debug(f"Searching for markdown files in: {root_dir}")
if not root_path.exists():
error_msg = f"Path does not exist: {root_dir}"
logger.error(error_msg)
raise ValueError(error_msg)
md_files = []
if root_path.is_file():
# If it's a specific file, check if it's a markdown file
if root_path.suffix.lower() == ".md":
md_files.append(root_path)
logger.debug(f"Added single markdown file: {root_path}")
else:
logger.warning(f"Specified file {root_path} is not a markdown file")
else:
# If it's a directory, find all .md files recursively
logger.debug(f"Scanning directory recursively: {root_path}")
for file_path in root_path.rglob("*.md"):
md_files.append(file_path)
logger.info(f"Found {len(md_files)} markdown files in {root_dir}")
logger.debug(f"Markdown files: {[str(f) for f in md_files]}")
return sorted(md_files)
def extract_markdown_links(
content: str, file_path: Path, logger: logging.Logger
) -> List[Tuple[str, int, str]]:
"""
Extract markdown links from file content.
Args:
content: File content as string
file_path: Path to the file being processed
logger: Logger instance for logging
Returns:
List of tuples: (link_text, line_number, link_url)
"""
links = []
logger.debug(f"Extracting markdown links from: {file_path}")
# Regex to match markdown links: [text](url)
# This captures the link text and URL, excluding image links (![text](url))
link_pattern = r"(?<!\!)\[([^\]]+)\]\(([^)]+)\)"
for line_num, line in enumerate(content.split("\n"), 1):
matches = re.finditer(link_pattern, line)
for match in matches:
link_text = match.group(1)
link_url = match.group(2)
# Only process links that could be to .md files
# Skip external URLs, anchors, and non-markdown files
if (
not link_url.startswith("http")
and not link_url.startswith("mailto:")
and not link_url.startswith("#")
and not link_url.startswith("ftp://")
and not link_url.startswith("//")
):
links.append((link_text, line_num, link_url))
logger.debug(
f"Found link at line {line_num}: [{link_text}]({link_url})"
)
logger.debug(f"Extracted {len(links)} potential markdown links from {file_path}")
return links
def resolve_link_path(
link_url: str,
source_file: Path,
logger: logging.Logger,
git_root_dir: Optional[str] = None,
) -> Path:
"""
Resolve a relative or absolute link to an absolute file path.
Args:
link_url: The link URL from the markdown file
source_file: The source markdown file path
logger: Logger instance for logging
git_root_dir: Git repository root directory for resolving GitHub-style absolute paths
Returns:
Resolved absolute path to the target file
"""
logger.debug(f"Resolving link: {link_url} from source: {source_file}")
# Remove any anchor/fragment from the URL
original_link_url = link_url
link_url = link_url.split("#")[0]
if not link_url:
logger.debug(f"Empty link URL, returning source file: {source_file}")
return source_file
# Handle GitHub-style absolute paths (starting with /) that should be relative to git repo root
if link_url.startswith("/") and git_root_dir:
# Strip the leading '/' and resolve relative to git repository root
repo_relative_path = link_url[1:] # Remove leading '/'
git_root_path = Path(git_root_dir)
resolved_path = git_root_path / repo_relative_path
final_path = resolved_path.resolve()
logger.debug(
f"GitHub-style absolute path resolved: {original_link_url} -> {final_path}"
)
return final_path
# If it's already an absolute path (and not a GitHub-style path), return as is
if os.path.isabs(link_url):
logger.debug(f"Absolute filesystem path detected: {link_url}")
return Path(link_url)
# Handle symlinks: resolve relative paths from the target's directory, not the symlink's directory
if source_file.is_symlink():
# Get the real path (target) of the symlink
real_source_file = source_file.resolve()
source_dir = real_source_file.parent
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
# Normalize the path (resolve any .. or . components)
final_path = resolved_path.resolve()
logger.debug(f"Resolved {original_link_url} -> {final_path}")
return final_path
def is_markdown_file(file_path: Path) -> bool:
"""Check if a file is a markdown file."""
return file_path.suffix.lower() == ".md"
def validate_links(
md_files: List[Path],
logger: logging.Logger,
git_root_dir: Optional[str] = None,
git_info: Optional[Dict[str, str]] = None,
) -> Dict[str, List[Dict]]:
"""
Validate all links in markdown files.
Args:
md_files: List of markdown files to validate
logger: Logger instance for logging
git_root_dir: Git repository root directory for path normalization
git_info: Git repository information for generating GitHub URLs
Returns:
Dictionary with file paths as keys and lists of broken links as values
"""
broken_links_report = {}
total_links_checked = 0
total_broken_links = 0
logger.info(f"Starting validation of {len(md_files)} markdown files")
for md_file in md_files:
logger.debug(f"Processing file: {md_file}")
try:
with open(md_file, "r", encoding="utf-8") as f:
content = f.read()
logger.debug(f"Successfully read {len(content)} characters from {md_file}")
except Exception as e:
logger.error(f"Could not read {md_file}: {e}")
continue
links = extract_markdown_links(content, md_file, logger)
broken_links = []
file_links_checked = 0
for link_text, line_num, link_url in links:
file_links_checked += 1
total_links_checked += 1
try:
resolved_path = resolve_link_path(
link_url, md_file, logger, git_root_dir
)
# Only check if the target should be a markdown file
if is_markdown_file(resolved_path):
if not resolved_path.exists():
# 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),
"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} -> {resolved_path}"
)
else:
logger.debug(
f"Valid link in {md_file}:{line_num} - {link_url} -> {resolved_path}"
)
else:
logger.debug(
f"Skipping non-markdown link in {md_file}:{line_num} - {link_url}"
)
except Exception as e:
# If we can't resolve the path, consider it 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": f"ERROR: {e}",
"github_url": github_url,
}
broken_links.append(broken_link_info)
total_broken_links += 1
logger.error(
f"Error resolving link in {md_file}:{line_num} - {link_url}: {e}"
)
if broken_links:
# Use git-relative path for the report key if git_root_dir is available
report_key = (
path_relative_to_git_root(md_file, git_root_dir, logger)
if git_root_dir
else str(md_file)
)
broken_links_report[report_key] = broken_links
logger.info(
f"File {md_file}: {len(broken_links)} broken links out of {file_links_checked} total links"
)
else:
logger.debug(f"File {md_file}: All {file_links_checked} links are valid")
logger.info(
f"Validation complete: {total_broken_links} broken links found out of {total_links_checked} total links checked"
)
return broken_links_report
def main():
parser = argparse.ArgumentParser(
description="Detect broken links in markdown files",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Check all markdown files in the current directory
python detect_broken_links.py
# Check markdown files in a specific directory
python detect_broken_links.py /path/to/docs
# Check markdown files in multiple directories
python detect_broken_links.py /path/to/docs1 /path/to/docs2
# Output to JSON file
python detect_broken_links.py --format json --output report.json
# Verbose logging
python detect_broken_links.py --verbose
# Log to file
python detect_broken_links.py --log-file debug.log
""",
)
parser.add_argument(
"directories",
nargs="*",
default=["."],
help="Directories to scan for markdown files (default: current directory)",
)
parser.add_argument(
"--output",
"-o",
type=str,
help="Output file for report (default: print to stdout)",
)
parser.add_argument(
"--format",
"-f",
choices=["json"],
default="json",
help="Output format (default: json)",
)
parser.add_argument(
"--verbose",
"-v",
action="store_true",
help="Print verbose output including file processing information",
)
parser.add_argument(
"--log-file", type=str, help="Log file path for detailed logging"
)
args = parser.parse_args()
# Set up logging
logger = setup_logging(verbose=args.verbose, log_file=args.log_file)
logger.info("Starting broken links detection")
logger.info(
f"Arguments: directories={args.directories}, format={args.format}, output={args.output}, verbose={args.verbose}"
)
# Get git repository information for path normalization
git_info = get_git_info(logger, args.directories[0] if args.directories else ".")
git_root_dir = git_info.get("git_root_dir") if git_info else None
all_broken_links = {}
total_files_processed = 0
all_processed_files = []
for directory in args.directories:
logger.info(f"Processing directory: {directory}")
try:
md_files = find_markdown_files(directory, logger)
total_files_processed += len(md_files)
# Convert file paths to git-relative paths for the report
if git_root_dir:
git_relative_files = [
path_relative_to_git_root(f, git_root_dir, logger) for f in md_files
]
all_processed_files.extend(git_relative_files)
else:
all_processed_files.extend([str(f) for f in md_files])
logger.info(f"Found {len(md_files)} markdown files in {directory}")
broken_links = validate_links(md_files, logger, git_root_dir, git_info)
all_broken_links.update(broken_links)
except Exception as e:
logger.error(f"Error processing directory {directory}: {e}")
continue
# Prepare the final report
report = {
"summary": {
"total_files_processed": total_files_processed,
"files_with_broken_links": len(all_broken_links),
"total_broken_links": sum(
len(links) for links in all_broken_links.values()
),
},
"broken_links": all_broken_links,
"all_processed_files": sorted(all_processed_files),
}
logger.info(f"Report summary: {report['summary']}")
# Generate JSON output
logger.info("Generating JSON report")
# Create a cleaned version of the report without resolved_path for JSON output
cleaned_broken_links = {}
for file_path, links in report["broken_links"].items():
cleaned_links = []
for link in links:
# Create a copy of the link without resolved_path
cleaned_link = {k: v for k, v in link.items() if k != "resolved_path"}
cleaned_links.append(cleaned_link)
cleaned_broken_links[file_path] = cleaned_links
cleaned_report = {
"summary": report["summary"],
"broken_links": cleaned_broken_links,
"all_processed_files": report["all_processed_files"],
}
output_content = json.dumps(cleaned_report, indent=2, ensure_ascii=False)
# Output the report
if args.output:
try:
logger.info(f"Writing report to: {args.output}")
with open(args.output, "w", encoding="utf-8") as f:
f.write(output_content)
logger.info(f"Report successfully written to: {args.output}")
print(f"Report written to: {args.output}", file=sys.stderr)
except Exception as e:
logger.error(f"Error writing to {args.output}: {e}")
print(f"Error writing to {args.output}: {e}", file=sys.stderr)
print(output_content)
else:
logger.info("Writing report to stdout")
print(output_content)
# Exit with error code if broken links were found
if all_broken_links:
logger.warning(
f"Exiting with error code 1 due to {len(all_broken_links)} files with broken links"
)
sys.exit(1)
else:
logger.info("No broken links found - exiting successfully")
if __name__ == "__main__":
main()
......@@ -76,3 +76,147 @@ jobs:
--exclude-all-private --exclude 0.0.0.0 \
$OFFLINE_FLAG \
.
broken-links-check:
name: Check for broken markdown links
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
# For pull_request events, use the PR head (commit from the contributor's branch/repo)
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
# No additional dependencies needed for the basic script
- name: Run broken links check
id: check-links
run: |
echo "Running broken links check on documentation files..."
# Run the broken links detection script and capture exit code
set +e # Don't exit immediately on error
python3 .github/workflows/detect_broken_links.py \
--verbose \
--format json \
--output broken-links-report.json \
.
exit_code=$?
set -e # Re-enable exit on error
# Check if the script found any broken links (exit code 1 means broken links found)
if [ $exit_code -eq 1 ]; then
echo "::error::Broken links found in documentation files"
echo "broken_links_found=true" >> $GITHUB_OUTPUT
elif [ $exit_code -eq 0 ]; then
echo "::notice::No broken links found"
echo "broken_links_found=false" >> $GITHUB_OUTPUT
else
echo "::error::Script failed with exit code $exit_code"
echo "broken_links_found=error" >> $GITHUB_OUTPUT
exit $exit_code
fi
- name: Display broken links and create annotations
if: steps.check-links.outputs.broken_links_found == 'true'
run: |
echo "::group::🔗 Broken Links Found"
# Parse and display the JSON report in a readable format, plus create GitHub annotations
python3 -c "
import json
import sys
import os
try:
with open('broken-links-report.json', 'r') as f:
report = json.load(f)
summary = report['summary']
broken_links = report['broken_links']
print('❌ BROKEN LINKS DETECTED')
print('=' * 50)
print(f'📊 Summary:')
print(f' • Total files processed: {summary[\"total_files_processed\"]}')
print(f' • Files with broken links: {summary[\"files_with_broken_links\"]}')
print(f' • Total broken links: {summary[\"total_broken_links\"]}')
print()
if broken_links:
print('🔍 Detailed Broken Links Report:')
print('-' * 40)
for file_path, links in broken_links.items():
print(f'\\n📄 File: {file_path}')
print(f' {len(links)} broken link(s) found')
print()
for i, link in enumerate(links, 1):
line = link['line']
link_text = link['link_text']
link_url = link['link_url']
github_url = link.get('github_url', '')
# Create GitHub annotation for each broken link
annotation_msg = f'Broken link: [{link_text}]({link_url})'
if github_url:
annotation_msg += f' - View: {github_url}'
print(f'::error file={file_path},line={line}::{annotation_msg}')
# Display in workflow output
print(f' {i}. Line {line}: [{link_text}]({link_url})')
if github_url:
print(f' 🔗 View on GitHub: {github_url}')
else:
print(f' 📍 Target: {link_url}')
print()
print('=' * 50)
print('✅ Next Steps:')
print('1. Check the annotations above in the Files Changed tab (for PRs)')
print('2. Click the GitHub links to jump directly to each broken link')
print('3. Fix all broken links before merging')
print('4. Re-run this workflow to verify fixes')
except Exception as e:
print(f'❌ Error reading broken links report: {e}')
sys.exit(1)
"
echo "::endgroup::"
- name: Alert for broken links
if: steps.check-links.outputs.broken_links_found == 'true' || steps.check-links.outputs.broken_links_found == 'error'
run: |
if [ "${{ steps.check-links.outputs.broken_links_found }}" = "error" ]; then
echo "::error::Workflow failed due to script error"
echo "Please check the workflow logs for details."
exit 1
else
echo "::error::❌ WORKFLOW FAILED: Broken links found in documentation files"
echo ""
echo "🔍 What to do next:"
echo "1. Check the 'Display broken links and create annotations' step above for details"
echo "2. Look for ::error annotations in the Files Changed tab (for PRs)"
echo "3. Click the GitHub URLs in the workflow output to jump directly to each broken link"
echo "4. Fix all broken links before merging"
echo ""
echo "💡 Each broken link shows:"
echo " - File name and line number where the broken link is located"
echo " - The broken link text and target URL"
echo " - A clickable GitHub URL to jump directly to the problem line"
exit 1
fi
......@@ -6,7 +6,7 @@ This directory contains utilities and manifests for Dynamo benchmarking and prof
**Before using these utilities, you must first set up Dynamo Cloud following the main installation guide:**
👉 **[Follow the Dynamo Cloud installation guide](../../guides/dynamo_deploy/installation_guide.md) to install the Dynamo Kubernetes Platform first.**
👉 **[Follow the Dynamo Cloud installation guide](/docs/guides/dynamo_deploy/installation_guide.md) to install the Dynamo Kubernetes Platform first.**
This includes:
1. Installing the Dynamo CRDs
......
......@@ -56,7 +56,7 @@ The framework is a Python-based wrapper around `genai-perf` that:
Follow these steps to benchmark Dynamo deployments:
### Step 1: Establish Kubernetes Cluster and Install Dynamo
Set up your Kubernetes cluster with NVIDIA GPUs and install the Dynamo Cloud platform. First follow the [installation guide](../../guides/dynamo_deploy/installation_guide.md) to install Dynamo Cloud, then use [deploy/utils/README](../../deploy/utils/README.md) to set up benchmarking resources.
Set up your Kubernetes cluster with NVIDIA GPUs and install the Dynamo Cloud platform. First follow the [installation guide](/docs/guides/dynamo_deploy/installation_guide.md) to install Dynamo Cloud, then use [deploy/utils/README](../../deploy/utils/README.md) to set up benchmarking resources.
### Step 2: Deploy DynamoGraphDeployments
Deploy your DynamoGraphDeployments separately using the [deployment documentation](../../components/backends/). Each deployment should have a frontend service exposed.
......
......@@ -46,7 +46,7 @@ Platform-specific deployment guides for production environments:
Low-level runtime examples for developers using Python<>Rust bindings:
- **[Hello World](runtime/hello_world/README.md)** - Minimal Dynamo runtime service demonstrating basic concepts
- **[Hello World](custom_backend/hello_world/README.md)** - Minimal Dynamo runtime service demonstrating basic concepts
## Getting Started
......
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