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
30 changes: 30 additions & 0 deletions .github/markdown-link-check-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"ignorePatterns": [
{
"pattern": "https://docs.zingg.ai/latest/.*",
"reason": "Live docs site - validated separately"
},
{
"pattern": "https://github.com/zinggAI/zingg/.*",
"reason": "GitHub repo links - validated by GitHub"
},
{
"pattern": "http://localhost.*",
"reason": "Local development URLs"
},
{
"pattern": "(py-modindex|genindex|search)\\.md",
"reason": "Sphinx-generated API index pages (produced at build time in pythonES/pythonEC)"
}
],
"retryOn429": true,
"retryCount": 3,
"fallbackRetryDelay": 5000,
"aliveStatusCodes": [200, 206],
"replacementPatterns": [
{
"pattern": "^/([^/])",
"replacement": "https://github.com/zinggAI/zingg/blob/main/$1"
}
]
}
128 changes: 128 additions & 0 deletions .github/scripts/validate_frontmatter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""
Validate frontmatter in markdown files using basic string parsing.
Checks for required fields: title, description, parent, nav_order (where applicable)
"""
import os
import sys
import re
from pathlib import Path

REQUIRED_FIELDS = ['description']
OPTIONAL_FIELDS = ['parent', 'nav_order', 'tags']

def parse_frontmatter(content):
"""Parse frontmatter from markdown content"""
# Match frontmatter between --- delimiters
match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
if not match:
return {}

frontmatter_text = match.group(1)
metadata = {}

# Simple YAML parsing for common cases
for line in frontmatter_text.split('\n'):
line = line.strip()
if not line or line.startswith('#'):
continue

# Handle key: value
if ':' in line:
key, value = line.split(':', 1)
key = key.strip()
value = value.strip()

# Remove quotes
if value.startswith('"') and value.endswith('"'):
value = value[1:-1]
elif value.startswith("'") and value.endswith("'"):
value = value[1:-1]

metadata[key] = value

return metadata

def validate_frontmatter(filepath):
"""Validate a single markdown file's frontmatter"""
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()

metadata = parse_frontmatter(content)

errors = []
warnings = []

# Check required fields
for field in REQUIRED_FIELDS:
if field not in metadata:
errors.append(f"Missing required field: {field}")
elif not metadata[field]:
errors.append(f"Empty required field: {field}")

# Check optional fields
for field in OPTIONAL_FIELDS:
if field not in metadata:
warnings.append(f"Missing optional field: {field}")

# Validate nav_order is integer if present
if 'nav_order' in metadata:
try:
int(metadata['nav_order'])
except (ValueError, TypeError):
errors.append(f"nav_order must be an integer, got: {metadata['nav_order']}")

return errors, warnings

except Exception as e:
return [f"Error reading file: {e}"], []

def main():
docs_dir = Path('docs')
if not docs_dir.exists():
print("ERROR: docs directory not found")
return 1

md_files = list(docs_dir.rglob('*.md'))
print(f"Found {len(md_files)} markdown files to validate")

total_errors = 0
total_warnings = 0

for md_file in md_files:
# Skip certain files
if any(part.startswith('.') for part in md_file.parts):
continue
if md_file.name in ['SUMMARY.md', 'README.md', 'CNAME']:
continue

errors, warnings = validate_frontmatter(md_file)

if errors:
print(f"\nERROR in {md_file}:")
for error in errors:
print(f" - {error}")
total_errors += len(errors)

if warnings:
print(f"\nWARNING in {md_file}:")
for warning in warnings:
print(f" - {warning}")
total_warnings += len(warnings)

print(f"\n{'='*50}")
print(f"Validation complete:")
print(f" Files checked: {len(md_files)}")
print(f" Errors: {total_errors}")
print(f" Warnings: {total_warnings}")

if total_errors > 0:
print("\nERROR: Frontmatter validation failed")
return 1
else:
print("\nSUCCESS: All frontmatter is valid")
return 0

if __name__ == '__main__':
sys.exit(main())
104 changes: 104 additions & 0 deletions .github/workflows/docs-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
name: Docs Validation

on:
pull_request:
branches: [main, master]
paths:
- 'docs/**'
- '.readthedocs.yaml'
- 'python/docs/**'
push:
branches: [main, master]
paths:
- 'docs/**'
- '.readthedocs.yaml'
- 'python/docs/**'

jobs:
validate-markdown:
name: Validate Markdown
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Check markdown links
uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: 'yes'
config-file: '.github/markdown-link-check-config.json'

- name: Check frontmatter
run: |
python3 .github/scripts/validate_frontmatter.py

- name: Check markdown formatting (markdownlint-cli2)
uses: DavidAnson/markdownlint-cli2-action@v24
with:
config: '.markdownlint-cli2.jsonc'
globs: 'docs/**/*.md'

validate-sphinx:
name: Validate Sphinx Build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install dependencies
run: |
cd python/docs
pip install -r ../../python/requirements.txt
pip install sphinx sphinx-rtd-theme sphinx-markdown-builder

- name: Build Sphinx docs
run: |
cd python/docs
make html

validate-readthedocs-config:
name: Validate ReadTheDocs Config
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Validate .readthedocs.yaml syntax
run: |
python3 -c "
import yaml
with open('.readthedocs.yaml') as f:
config = yaml.safe_load(f)
print('ReadTheDocs config is valid YAML')
print(f'Version: {config.get(\"version\")}')
print(f'Python version: {config.get(\"build\", {}).get(\"tools\", {}).get(\"python\")}')
"

validate-version-consistency:
name: Check Version Consistency
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Check version in conf.py
run: |
VERSION=$(grep -E "version\s*=\s*['\"]([^'\"]+)['\"]" python/docs/conf.py | head -1 | sed -E "s/.*['\"]([^'\"]+)['\"].*/\1/")
echo "Sphinx version: $VERSION"
if [[ -z "$VERSION" ]]; then
echo "ERROR: Could not find version in python/docs/conf.py"
exit 1
fi

- name: Check for old version references in docs
run: |
if grep -r "0\.6\.0" docs/ --include="*.md" 2>/dev/null; then
echo "WARNING: Found references to version 0.6.0 in docs"
else
echo "No old version references found"
fi
88 changes: 88 additions & 0 deletions .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
{
// =========================================================================
// Zingg documentation markdownlint configuration
// -------------------------------------------------------------------------
// The Zingg docs are authored for GitBook. GitBook uses its own block
// syntax ({% hint %}, {% tabs %}, {% stepper %}, {% embed %}), inline HTML
// (<details>, <figure>, <table>, <a>), bare URLs, and compact tables.
// Docs are also synced from the GitBook CLI, which exports Markdown that
// doesn't conform to strict CommonMark conventions (heading levels across
// tabs, trailing spaces, hard tabs inside code, single-pipe tables).
//
// As a result, only a subset of markdownlint rules can be meaningfully
// enforced. Disabled rules are grouped here with the reason each one is
// turned off. The remaining enabled rules act as a lightweight but useful
// gate for new and edited content.
// =========================================================================

"config": {
// ---------- GitBook syntax compatibility (permanently disabled) --------
// MD013: Line length (hints, tables, bare URLs exceed 80 chars)
// MD025/MD041: Frontmatter precedes every H1
// MD024: Duplicate headings occur across steps and tabs
// MD033: Inline HTML is required for GitBook blocks
// MD034: Bare URLs are used in hints/embeds
// MD036: Emphasis-instead-of-heading is used intentionally
// MD040: Bare code fences and mermaid blocks omit language tags
"MD013": false,
"MD025": false,
"MD041": false,
"MD024": false,
"MD033": false,
"MD034": false,
"MD036": false,
"MD040": false,

// ---------- GitBook CLI export artifacts (currently too noisy) ---------
// MD001: Heading levels "skip" between tab/step sub-headings
// MD004: Mixed unordered-list markers from tooling
// MD007: Unordered-list indentation from tooling
// MD009: Trailing spaces introduced by the GitBook CLI export
// MD010: Hard tabs inside fenced code blocks (valid in code samples)
// MD014: Dollar signs preceding shell commands in code samples
// MD019: Multiple spaces after atx heading marker
// MD026: Trailing punctuation in headings (many contain ':', '!')
// MD028: Blank line inside blockquote (common in nested hints)
// MD029: Ordered-list item prefixes from numbering carried over from docs
// MD030: Spaces after list markers
// MD037: Spaces inside emphasis markers (rendered text uses " **x** ")
// MD038: Spaces inside code span markers
// MD039: Spaces inside link text
// MD045: Images without alt text (GitBook figure blocks)
// MD047: Files must end with a single newline
// MD051: Link fragments (anchors target GitBook-generated headings)
// MD059: Descriptive link text (inline links are intentionally terse)
// MD060: Table column style (compact single-pipe GitBook tables)
"MD001": false,
"MD004": false,
"MD007": false,
"MD009": false,
"MD010": false,
"MD012": false,
"MD014": false,
"MD019": false,
"MD022": false,
"MD026": false,
"MD028": false,
"MD029": false,
"MD030": false,
"MD031": false,
"MD032": false,
"MD037": false,
"MD038": false,
"MD039": false,
"MD045": false,
"MD047": false,
"MD051": false,
"MD059": false,
"MD060": false

// ---- Remaining rules stay at their defaults as a lightweight gate ----
// MD003 (heading-style), MD018-MD021 (space-after-heading), MD023
// (space-before-heading), MD035 (hr-style), MD046 (code-block-style),
// MD048 (code-fence-style), MD049/MD050 (emphasis-style).
},

"globs": ["docs/**/*.md"],
"ignores": ["docs/.gitbook/**"]
}
Loading