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
79 changes: 79 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Copyright (C) 2026 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0

name: deploy-docs

on:
push:
branches:
- main
paths:
- 'README.md'
- 'website/**'
- '.github/workflows/deploy-docs.yml'
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

# Concurrency control - only one deployment at a time
concurrency:
group: "pages"
cancel-in-progress: false

jobs:
build:
name: Build Documentation
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: '3.12'
cache: 'pip'
cache-dependency-path: website/requirements.txt

- name: Install dependencies
working-directory: website
run: |
pip install --upgrade pip
pip install -r requirements.txt

- name: Build MkDocs site
working-directory: website
run: mkdocs build --strict --verbose

- name: Upload artifact
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
with:
path: ./website/site

deploy:
name: Deploy to GitHub Pages
needs: build
runs-on: ubuntu-24.04
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ go.work
# Build artifacts
/kortex-cli
dist/

# MkDocs build output
website/site/
website/docs/*.md
41 changes: 41 additions & 0 deletions website/WEBSITE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Website Documentation

This directory contains the MkDocs configuration for automatically generating the kortex-cli documentation website from the repository's README.md.

## How It Works

1. **Single Source**: The project `README.md` (in the repository root) is the only file you edit
2. **Automatic Splitting**: The `hooks.py` script splits README.md by `##` headings into separate pages
3. **Build & Deploy**: GitHub Actions automatically builds and deploys the site on merge to main

## Local Preview

```bash
# From the website/ directory:
cd website

# Create and activate virtual environment (first time only)
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate

# Install dependencies (first time only)
pip install -r requirements.txt

# Start preview server
mkdocs serve

# Open http://127.0.0.1:8000 in browser
```

## Files

- **mkdocs.yml** - MkDocs configuration (theme, plugins, SEO settings)
- **hooks.py** - Custom hook that splits README.md into multiple pages
- **requirements.txt** - Python dependencies (MkDocs, Material theme, plugins)
- **docs/** - Temporary directory for generated markdown files (ignored by git)

## Deployment

The website is automatically deployed to GitHub Pages when changes are pushed to the `main` branch.

Website URL: https://kortex-hub.github.io/kortex-cli/
2 changes: 2 additions & 0 deletions website/docs/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This directory is used by MkDocs during the build process.
# Markdown files are automatically generated from README.md via hooks.py
207 changes: 207 additions & 0 deletions website/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
#!/usr/bin/env python3
# Copyright (C) 2026 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0

"""
MkDocs hooks for automatically splitting README.md into multiple pages.

This hook reads README.md and creates separate pages for each ## (level 2) heading.
The pages are created as virtual files during the build process, so README.md remains
the single source of truth.
"""

import re
import logging
from pathlib import Path

logger = logging.getLogger("mkdocs.plugins")


def slugify(text):
"""Convert heading text to a URL-friendly slug."""
# Remove special characters and convert to lowercase
slug = re.sub(r'[^\w\s-]', '', text.lower())
# Replace spaces with hyphens
slug = re.sub(r'[\s_]+', '-', slug)
# Remove leading/trailing hyphens
slug = slug.strip('-')
return slug


def split_readme_content(content):
"""
Split README.md content into sections based on ## headings.

Returns:
list of dict: Each dict contains 'title', 'slug', and 'content'
"""
sections = []
lines = content.split('\n')

# First section: everything before first ## heading (becomes index.md)
current_section = {
'title': 'Home',
'slug': 'index',
'content': []
}

i = 0
# Capture content before first ## heading
while i < len(lines):
line = lines[i]
if line.startswith('## '):
break
current_section['content'].append(line)
i += 1

sections.append(current_section)

# Process each ## section
while i < len(lines):
line = lines[i]

if line.startswith('## '):
# Start new section
title = line[3:].strip() # Remove "## "
slug = slugify(title)

current_section = {
'title': title,
'slug': slug,
'content': [] # Don't include the heading (MkDocs shows it as page title)
}
sections.append(current_section)
else:
# Add to current section
current_section['content'].append(line)

i += 1

# Join content lines back into strings
for section in sections:
section['content'] = '\n'.join(section['content'])

return sections


def on_files(files, config):
"""
Called after files are collected from docs_dir.
Split README.md into multiple virtual files.
"""
from mkdocs.structure.files import File

# Read README.md from project root
# docs_dir is website/docs -> parent is website -> parent is repo root
docs_dir_abs = Path(config['docs_dir']).resolve()
readme_path = docs_dir_abs.parent.parent / 'README.md'

if not readme_path.exists():
logger.warning(f"README.md not found at {readme_path}")
return files

logger.info(f"Splitting README.md from {readme_path}")

with open(readme_path, 'r', encoding='utf-8') as f:
readme_content = f.read()

# Split README into sections
sections = split_readme_content(readme_content)

logger.info(f"Found {len(sections)} sections in README.md")

# Remove existing index.md symlink if it exists
files_to_remove = [f for f in files if f.src_path == 'index.md']
for f in files_to_remove:
files.remove(f)

# Create virtual files for each section
for section in sections:
filename = f"{section['slug']}.md"

# Create a temporary file
temp_path = Path(config['docs_dir']) / filename
temp_path.write_text(section['content'], encoding='utf-8')

# Create MkDocs File object
file_obj = File(
path=filename,
src_dir=config['docs_dir'],
dest_dir=config['site_dir'],
use_directory_urls=config['use_directory_urls']
)

files.append(file_obj)
logger.debug(f"Created virtual file: {filename} ({section['title']})")

return files


def on_nav(nav, config, files):
"""
Called after navigation is created.
Build navigation structure from split sections.
"""
from mkdocs.structure.nav import Navigation

# Read README.md again to get section titles in order
# Use same path calculation as on_files for consistency
docs_dir_abs = Path(config['docs_dir']).resolve()
readme_path = docs_dir_abs.parent.parent / 'README.md'

if not readme_path.exists():
return nav

with open(readme_path, 'r', encoding='utf-8') as f:
readme_content = f.read()

sections = split_readme_content(readme_content)

# Build navigation items
nav_items = []
for section in sections:
filename = f"{section['slug']}.md"

# Find corresponding file
file_obj = None
for f in files:
if f.src_path == filename:
file_obj = f
break

if file_obj:
from mkdocs.structure.pages import Page
page = Page(section['title'], file_obj, config)
nav_items.append(page)

# Create new navigation
nav.items = nav_items
nav.pages = nav_items

return nav


def on_post_build(config):
"""
Called after site is built.
Clean up temporary markdown files created during build.
"""
docs_dir = Path(config['docs_dir'])

# Remove all .md files in docs_dir (they were created temporarily)
for md_file in docs_dir.glob('*.md'):
md_file.unlink()
logger.debug(f"Cleaned up temporary file: {md_file}")
Loading
Loading