Skip to content
Merged
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
385 changes: 6 additions & 379 deletions .github/workflows/update-api-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,387 +73,14 @@ jobs:
echo "KUBE_VERSION=$KUBE_VERSION" >> $GITHUB_ENV

- name: Generate all documentation for selected version(s)
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
GITHUB_RELEASE_TAG: ${{ github.event.release.tag_name }}
INPUT_VERSION: ${{ inputs.version }}
KUBE_VERSION: ${{ env.KUBE_VERSION }}
run: |
cd kgateway.dev

# Process selected version(s) sequentially - generate all doc types per version
python3 -c "
import json
import sys
import subprocess
import os
import re

def resolve_tag_for_version(version, link_version):
'''Resolve the git tag to use for a given version (for release triggers)'''
if link_version == 'main':
return 'main'

# For other linkVersions, get the latest tag matching the version pattern
try:
result = subprocess.run(['git', 'ls-remote', '--tags', '--sort=-version:refname', 'https://github.com/kgateway-dev/kgateway.git'],
capture_output=True, text=True, check=True)
if result.stdout.strip():
# Filter tags that match our version pattern
# Create regex pattern: 2.1.x becomes v2\.1\.\d+
version_pattern = version.replace('.x', r'\.\d+')
pattern = f'v{version_pattern}'
matching_tags = []
for line in result.stdout.strip().split('\n'):
if 'refs/tags/' in line:
tag_name = line.split('/')[-1]
if re.match(pattern, tag_name) and not any(suffix in tag_name for suffix in ['-rc', '-beta', '-main', '-agw']):
matching_tags.append(tag_name)

if matching_tags:
# Sort by version and take the latest
matching_tags.sort(key=lambda x: [int(n) for n in x.replace('v', '').split('.')], reverse=True)
return matching_tags[0]
else:
print(f'No stable tags found for version {version}')
return None
else:
print(f'No tags found in repository for version {version}')
return None
except subprocess.CalledProcessError as e:
print(f'Error fetching tags for version {version}: {e}')
return None

def resolve_branch_for_version(version, link_version):
'''Resolve the git branch to use for a given version (for manual triggers)'''
if link_version == 'main':
return 'main'

# For other linkVersions, construct the branch name from the version
# e.g., version "2.1.x" -> branch "v2.1.x"
branch_name = f'v{version}'

# Verify the branch exists in the remote repository
try:
result = subprocess.run(['git', 'ls-remote', '--heads', 'https://github.com/kgateway-dev/kgateway.git', branch_name],
capture_output=True, text=True, check=True)
if result.stdout.strip():
print(f' Found branch: {branch_name}')
return branch_name
else:
print(f' Warning: Branch {branch_name} not found, trying to use it anyway')
# Still return the branch name - clone will fail if it doesn't exist
return branch_name
except subprocess.CalledProcessError as e:
print(f' Warning: Error checking branch {branch_name}: {e}')
# Still return the branch name - clone will fail if it doesn't exist
return branch_name

def clone_repository(ref, kgateway_dir='kgateway'):
'''Clone the kgateway repository at the specified branch or tag'''
# Clean up any existing directory
if os.path.exists(kgateway_dir):
subprocess.run(['rm', '-rf', kgateway_dir], check=True)

# Clone repository
if ref == 'main':
subprocess.run(['git', 'clone', '--branch', 'main', '--depth', '1', 'https://github.com/kgateway-dev/kgateway.git', kgateway_dir], check=True)
else:
subprocess.run(['git', 'clone', '--depth', '1', '--branch', ref, 'https://github.com/kgateway-dev/kgateway.git', kgateway_dir], check=True)

def generate_api_docs(version, link_version, url_path, kgateway_dir='kgateway'):
'''Generate API reference documentation'''
print(f' → Generating API docs for version {version}')

# Check if the API directory exists
api_path = f'{kgateway_dir}/api/v1alpha1/'
if not os.path.exists(api_path):
print(f' Warning: API directory {api_path} does not exist, skipping API docs')
return False

# Generate API docs using individual subprocess calls
subprocess.run(['envsubst'], input=open('scripts/crd-ref-docs-config.yaml').read(), text=True, stdout=open(f'crd-ref-docs-config-{link_version}.yaml', 'w'))

subprocess.run([
'go', 'run', 'github.com/elastic/crd-ref-docs@v0.1.0',
f'--source-path={api_path}',
'--renderer=markdown',
'--output-path=./',
f'--config=crd-ref-docs-config-{link_version}.yaml'
], check=True)

os.remove(f'crd-ref-docs-config-{link_version}.yaml')

# Read the generated content once
with open('./out.md') as f:
generated_content = f.read()

os.remove('./out.md')

# Generate docs for both envoy and agentgateway directories
for doc_dir in ['envoy', 'agentgateway']:
target_path = f'content/docs/{doc_dir}/{url_path}/reference/'
os.makedirs(target_path, exist_ok=True)

api_file = f'{target_path}api.md'

# Create API reference file with frontmatter
with open(api_file, 'w') as f:
f.write('---\n')
f.write('title: API reference\n')
f.write('weight: 10\n')
f.write('---\n\n')
f.write(generated_content)

# Format the generated docs with sed commands
subprocess.run(['sed', '-i', 's/Required: {}/Required/g', api_file], check=True)
subprocess.run(['sed', '-i', 's/Optional: {}/Optional/g', api_file], check=True)
subprocess.run(['sed', '-i', '/^# API Reference$/,/^$/d', api_file], check=True)

# Additional post-processing to clean up complex struct types
import re
with open(api_file, 'r') as f:
content = f.read()

# Replace complex struct type definitions with simple "struct"
# Pattern matches: _Underlying type:_ _[struct{...}...]_
content = re.sub(
r'_Underlying type:_ _\[struct\{[^\]]+\}\]\([^\)]+\)_',
'_Underlying type:_ _struct_',
content
)

# Handle empty struct{} patterns
content = re.sub(
r'_Underlying type:_ _\[struct\{\}\]\(#struct\{\}\)_',
'_Underlying type:_ _struct_',
content
)

# Also handle cases without the link wrapper
content = re.sub(
r'_Underlying type:_ _struct\{[^\}]+\}_',
'_Underlying type:_ _struct_',
content
)

with open(api_file, 'w') as f:
f.write(content)

print(f' ✓ Generated API docs in {api_file}')

return True

def generate_helm_docs(version, link_version, url_path, kgateway_dir='kgateway'):
'''Generate Helm chart reference documentation'''
print(f' → Generating Helm docs for version {version}')

# Generate Helm docs for each chart
charts = ['kgateway:kgateway', 'kgateway-crds:kgateway-crds']
generated_any = False

for chart in charts:
dir_name, file_name = chart.split(':')
helm_path = f'{kgateway_dir}/install/helm/{dir_name}'

# Check if Helm directory exists
if not os.path.exists(helm_path):
print(f' Warning: Helm directory {helm_path} does not exist, skipping {file_name}')
continue

result = subprocess.run([
'go', 'run', 'github.com/norwoodj/helm-docs/cmd/helm-docs@v1.14.2',
f'--chart-search-root={helm_path}',
'--dry-run'
], capture_output=True, text=True, check=True)

# Write the raw helm-docs output to assets directory
# Use actual version numbers (2.2.x, 2.1.x, etc.) not linkVersion (main, latest)
# This prevents overwriting when promoting versions
assets_path = f'assets/docs/pages/reference/helm/{version}/'
os.makedirs(assets_path, exist_ok=True)

helm_file = f'{assets_path}{file_name}.md'

with open(helm_file, 'w') as f:
f.write(result.stdout)

# Remove badge line and following empty line
subprocess.run(['sed', '-i', '/!\[Version:/,/^$/d', helm_file], check=True)

# Remove the title (# heading) and description lines from the top
# These will be hardcoded in the content files instead
# Remove lines 1-3 which contain: title, blank line, description
subprocess.run(['sed', '-i', '1,3d', helm_file], check=True)

# Add a note for charts with no configurable values (like kgateway-crds)
with open(helm_file, 'r', encoding='utf-8') as f:
content = f.read()

# Normalize any bare type=info callouts before further processing
content = content.replace('{{< callout type=info >}}', '{{< callout type="info" >}}')

# If no values table, append callout (once). Otherwise normalize callout quotes.
if '## Values' not in content or ('## Values' in content and '|-----|' not in content):
note = '\n\n{{< callout type="info" >}}\nNo configurable values are currently available for this chart.\n{{< /callout >}}\n'
if '{{< callout' not in content:
content = content.rstrip() + note

# Swap Default and Description columns in the values table
swapped_lines = []
in_table = False
for line in content.splitlines():
stripped = line.strip()
if stripped.startswith('| Key ') and 'Default' in line and 'Description' in line:
in_table = True
swapped_lines.append('| Key | Type | Description | Default |')
continue
if in_table and stripped.startswith('|-----'):
swapped_lines.append('|-----|------|-------------|---------|')
continue
if in_table:
if not stripped.startswith('|'):
in_table = False
swapped_lines.append(line)
continue
parts = [p.strip() for p in line.split('|')]
# Expect 6 elements with leading/trailing blanks; ensure we have enough
if len(parts) >= 6:
key, typ, default, desc = parts[1], parts[2], parts[3], parts[4]
swapped_lines.append(f'| {key} | {typ} | {desc} | {default} |')
else:
swapped_lines.append(line)
else:
swapped_lines.append(line)
content = '\n'.join(swapped_lines)

with open(helm_file, 'w', encoding='utf-8') as f:
f.write(content)

print(f' ✓ Generated Helm docs in {helm_file}')

generated_any = True

return generated_any

def generate_metrics_docs(version, link_version, url_path, kgateway_dir='kgateway'):
'''Generate control plane metrics documentation'''
print(f' → Generating metrics docs for version {version}')

os.makedirs(f'assets/docs/snippets/{link_version}', exist_ok=True)

# Check if metrics tool exists
metrics_tool_path = f'{kgateway_dir}/pkg/metrics/cmd/findmetrics/main.go'
if not os.path.exists(metrics_tool_path):
print(f' Warning: Metrics tool {metrics_tool_path} does not exist, skipping metrics docs')
return False

# Run the metrics finder tool
result = subprocess.run([
'go', 'run', metrics_tool_path,
'--markdown', f'./{kgateway_dir}'
], capture_output=True, text=True, check=True)

with open(f'assets/docs/snippets/{link_version}/metrics-control-plane.md', 'w') as f:
f.write(result.stdout)

print(f' ✓ Generated metrics docs in assets/docs/snippets/{link_version}/metrics-control-plane.md')
return True

# Main processing logic - determine target version
is_release_trigger = '${{ github.event_name }}' == 'release'

if is_release_trigger:
# Release trigger: determine version from release tag
release_tag = '${{ github.event.release.tag_name }}'
print(f'🎉 Release trigger detected: {release_tag}')

# Parse release tag (e.g., "v2.1.1") to version family (e.g., "2.1.x")
if release_tag.startswith('v'):
version_parts = release_tag[1:].split('.') # Remove 'v' and split
if len(version_parts) >= 2:
target_version = f'{version_parts[0]}.{version_parts[1]}.x'
print(f'📋 Mapped release {release_tag} to version family: {target_version}')
else:
print(f'❌ Invalid release tag format: {release_tag}')
sys.exit(1)
else:
print(f'❌ Release tag does not start with "v": {release_tag}')
sys.exit(1)
else:
# Manual dispatch: use input version
target_version = '${{ inputs.version }}'
print(f'👤 Manual trigger with version: {target_version}')

with open('versions.json', 'r') as f:
versions = json.load(f)

# Filter versions based on target
if target_version != 'all':
all_versions = versions # Keep original list for error reporting
versions = [v for v in versions if v['version'] == target_version]
if not versions:
print(f'❌ Version {target_version} not found in versions.json')
print(f'📋 Available versions: {[v["version"] for v in all_versions]}')
sys.exit(1)

print(f'Processing {len(versions)} version(s): {[v[\"version\"] for v in versions]}')

for version_info in versions:
version = version_info['version']
link_version = version_info['linkVersion']
url_path = version_info['url']

print(f'\n🔄 Processing version: {version} (linkVersion: {link_version}, path: {url_path})')

# Resolve tag or branch based on trigger type
if is_release_trigger:
# Use tags for release triggers
ref = resolve_tag_for_version(version, link_version)
ref_type = 'tag'
else:
# Use branches for manual triggers
ref = resolve_branch_for_version(version, link_version)
ref_type = 'branch'

if not ref:
print(f'❌ Skipping version {version} - could not resolve {ref_type}')
continue

print(f' Using {ref_type}: {ref}')

# Clone repository once per version
try:
clone_repository(ref)
subprocess.run(['git', '-C', 'kgateway', 'rev-parse', 'HEAD'], check=True)
print(f' ✓ Cloned repository')
except subprocess.CalledProcessError as e:
print(f'❌ Failed to clone repository for version {version}: {e}')
continue

# Generate all documentation types for this version
success_count = 0

try:
if generate_api_docs(version, link_version, url_path):
success_count += 1
except Exception as e:
print(f' ⚠ API docs failed: {e}')

try:
if generate_helm_docs(version, link_version, url_path):
success_count += 1
except Exception as e:
print(f' ⚠ Helm docs failed: {e}')

try:
if generate_metrics_docs(version, link_version, url_path):
success_count += 1
except Exception as e:
print(f' ⚠ Metrics docs failed: {e}')

# Clean up repository after processing this version
subprocess.run(['rm', '-rf', 'kgateway'], check=True)

print(f'✅ Completed version {version} - generated {success_count}/3 doc types')

print('\n🎉 All versions processed!')
"
python3 scripts/generate-ref-docs.py


- name: Clean up temporary directories
Expand Down
Loading