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
78 changes: 63 additions & 15 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,78 @@ jobs:
NVIDIA_DRIVER_CAPABILITIES: all
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DISABLE_REQUIRE: 1
DOCS_MAX_VERSIONS: "4" # Max number of release versions to keep
container: *container_template
steps:
- uses: actions/checkout@v4

- name: Cache Python dependencies
id: cache-pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-docs-${{ hashFiles('docs/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-docs-

- name: Restore previous docs output
if: github.event_name == 'push'
uses: actions/cache@v4
with:
path: docs/build/html
key: docs-output-${{ github.repository }}-${{ github.ref_name }}
restore-keys: |
docs-output-${{ github.repository }}-${{ github.ref_name }}-
docs-output-${{ github.repository }}-

- name: Build docs
shell: bash
run: |
pip install -e . --extra-index-url http://pyp.open3dv.site:2345/simple/ --trusted-host pyp.open3dv.site
pip install -r docs/requirements.txt
cd ${GITHUB_WORKSPACE}/docs
echo "Start Building docs..."
pip uninstall pymeshlab -y
pip install pymeshlab==2023.12.post3
make html

if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
VERSION="${GITHUB_REF_NAME}"
echo "Building docs for release tag ${VERSION}..."

# Build only this version into its own subdirectory
sphinx-build source build/html/${VERSION}

cd build/html

# Prune old release versions beyond the window
mapfile -t TAG_DIRS < <(ls -d v*/ 2>/dev/null | sort -V)
while [[ ${#TAG_DIRS[@]} -gt ${DOCS_MAX_VERSIONS} ]]; do
echo "Pruning old version: ${TAG_DIRS[0]}"
rm -rf "${TAG_DIRS[0]}"
TAG_DIRS=("${TAG_DIRS[@]:1}")
done

# Generate versions.json and root index.html
python3 ${GITHUB_WORKSPACE}/docs/scripts/generate_versions_json.py \
--build-dir .

else
echo "Building dev docs for main branch..."
# Build only main/ — don't touch existing version directories
rm -rf build/html/main
sphinx-build source build/html/main

cd build/html

# Generate versions.json and root index.html
python3 ${GITHUB_WORKSPACE}/docs/scripts/generate_versions_json.py \
--build-dir .
fi

Comment on lines 72 to +114
Comment on lines 72 to +114
- name: Upload docs artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
if: github.event_name == 'push'
uses: actions/upload-pages-artifact@v3
with:
with:
path: ${{ github.workspace }}/docs/build/html
retention-days: 3

test:
if: github.event_name == 'pull_request'
Expand All @@ -86,19 +140,13 @@ jobs:
pytest tests

publish:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
if: github.event_name == 'push'
needs: build
runs-on: Linux
permissions:
pages: write
id-token: write
env:
NVIDIA_DRIVER_CAPABILITIES: all
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DISABLE_REQUIRE: 1
container: *container_template
id-token: write
steps:
- uses: actions/checkout@v4
- name: Download docs artifact
uses: actions/download-artifact@v4
with:
Expand All @@ -120,7 +168,7 @@ jobs:
# steps:
# - uses: actions/checkout@v4
# with:
# fetch-depth: 0
# fetch-depth: 0

# - name: (Release) Install build tools
# run: |
Expand All @@ -144,4 +192,4 @@ jobs:
# - name: (Release) Publish to PyPI
# uses: pypa/gh-action-pypi-publish@release/v1
# with:
# password: ${{ secrets.PYPI_API_TOKEN }}
# password: ${{ secrets.PYPI_API_TOKEN }}
7 changes: 7 additions & 0 deletions docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ help:
%: Makefile
@rm -rf "$(BUILDDIR)"
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

# Build current version only (for local development / PR verification)
.PHONY: current-docs
current-docs:
@rm -rf "$(BUILDDIR)/html"
@$(SPHINXBUILD) -W --keep-going "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) $(O)
@python3 "$(CURDIR)/scripts/generate_versions_json.py" --build-dir "$(BUILDDIR)/html"
3 changes: 1 addition & 2 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@ myst-parser
sphinx-autosummary-accessors
sphinxcontrib-bibtex
sphinx-design
sphinx_autodoc_typehints
sphinx-multiversion
sphinx_autodoc_typehints
97 changes: 97 additions & 0 deletions docs/scripts/build_versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# 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.
# ----------------------------------------------------------------------------

"""Helper script for filtering versions to maintain buffer size."""

import re
from pathlib import Path


def parse_version(tag: str) -> tuple[int, int, int]:
"""Parse a version tag like 'v1.2.3' into a tuple (1, 2, 3)."""
match = re.match(r"^v(\d+)\.(\d+)\.(\d+)$", tag)
if not match:
return (0, 0, 0)
return (int(match.group(1)), int(match.group(2)), int(match.group(3)))


def filter_versions(
all_versions: list[str],
buffer_size: int,
main_branch: str = "main",
) -> list[str]:
"""Filter versions to maintain buffer size.

Keeps the latest (buffer_size - 1) release versions plus the main branch.

Args:
all_versions: List of all available version references
buffer_size: Total number of versions to keep (releases + main)
main_branch: Name of the main branch

Returns:
List of versions to keep
"""
# Separate releases from branches
releases = [v for v in all_versions if re.match(r"^v\d+\.\d+\.\d+$", v)]
branches = [v for v in all_versions if v not in releases]

# Sort releases by version (newest first)
releases.sort(key=parse_version, reverse=True)

# Keep latest (buffer_size - 1) releases
releases_to_keep = releases[: (buffer_size - 1)]

Comment on lines +53 to +57
# Always include main branch if it exists
versions_to_keep = releases_to_keep.copy()
if main_branch in branches:
versions_to_keep.append(main_branch)

return versions_to_keep


def main():
"""CLI entry point for version filtering."""
import argparse

parser = argparse.ArgumentParser(
description="Filter versions for multi-version docs"
)
parser.add_argument(
"--versions",
nargs="+",
required=True,
help="List of all available versions",
)
parser.add_argument(
"--buffer-size",
type=int,
default=5,
help="Total number of versions to keep (releases + main)",
)
parser.add_argument(
"--main-branch",
default="main",
help="Name of the main branch",
)
args = parser.parse_args()

filtered = filter_versions(args.versions, args.buffer_size, args.main_branch)
print(" ".join(filtered))


if __name__ == "__main__":
main()
112 changes: 112 additions & 0 deletions docs/scripts/generate_versions_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# 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.
# ----------------------------------------------------------------------------
"""Generate versions.json and root index.html for the docs version selector."""

from __future__ import annotations

import argparse
import json
import re
from pathlib import Path


def parse_version(tag: str) -> tuple[int, int, int]:
"""Parse a version tag like 'v1.2.3' into a tuple (1, 2, 3)."""
match = re.match(r"^v(\d+)\.(\d+)\.(\d+)$", tag)
if not match:
return (0, 0, 0)
return (int(match.group(1)), int(match.group(2)), int(match.group(3)))


def main() -> None:
parser = argparse.ArgumentParser(
description="Generate versions.json and root index.html for multi-version docs"
)
parser.add_argument(
"--build-dir",
default="build/html",
help="Path to build/html directory (default: build/html)",
)
parser.add_argument(
"--output",
default=None,
help="Output path for versions.json (default: <build-dir>/versions.json)",
)
parser.add_argument(
"--latest",
default=None,
help="Name of the latest stable version (default: auto-detected from tags, falls back to main)",
)
args = parser.parse_args()

html_dir = Path(args.build_dir)
output = Path(args.output) if args.output else html_dir / "versions.json"

if not html_dir.exists():
print(f"Error: Build directory '{html_dir}' does not exist.")
raise SystemExit(1)

versions: list[dict[str, str]] = []

# Collect tag versions (vX.Y.Z directories), sorted newest-first
tag_dirs = sorted(
[d for d in html_dir.glob("v*") if d.is_dir()],
key=lambda d: parse_version(d.name),
reverse=True,
)
for d in tag_dirs:
name = d.name
versions.append({"name": name, "url": f"./{name}/index.html", "type": "tag"})

# Collect main (dev branch)
if (html_dir / "main").is_dir():
versions.append({"name": "main", "url": "./main/index.html", "type": "branch"})

# Determine latest: explicit arg > newest tag > main
if args.latest:
latest = args.latest
elif versions:
tag_names = [v["name"] for v in versions if v["type"] == "tag"]
latest = tag_names[0] if tag_names else "main"
else:
latest = "main"

manifest = {
"latest": latest,
"versions": versions,
}

# Write versions.json
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(manifest, indent=2))
print(f"Generated {output} with {len(versions)} versions (latest: {latest})")

# Write root index.html redirect
index_path = html_dir / "index.html"
index_content = (
"<!DOCTYPE html>\n"
"<html><head>\n"
f" <title>EmbodiChain Docs</title>\n"
f' <meta http-equiv="refresh" content="0; url=./{latest}/index.html">\n'
"</head></html>\n"
)
index_path.write_text(index_content)
print(f"Generated {index_path} (redirects to ./{latest}/index.html)")
Comment on lines +98 to +108


if __name__ == "__main__":
main()
36 changes: 36 additions & 0 deletions docs/source/_static/version-redirect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Version redirect script for multi-version documentation.
* Redirects to the latest stable release version, or falls back to 'main'.
*/

(function() {
'use strict';

// Try to fetch versions.json (generated by generate_versions_json.py)
fetch('versions.json')
.then(response => {
if (!response.ok) {
throw new Error('versions.json not found');
}
Comment on lines +1 to +14
return response.json();
})
.then(data => {
// Get the latest version from the JSON
const latestVersion = data.latest || data.versions?.[0]?.name || 'main';

const currentPath = window.location.pathname;

// If we're at root, redirect to latest version
if (currentPath === '/' || currentPath.endsWith('/index.html') || currentPath.endsWith('/')) {
window.location.href = latestVersion + '/';
}
Comment on lines +21 to +26
Comment on lines +1 to +26
})
.catch(error => {
console.warn('Version redirect failed:', error.message);
// Fallback to main on error
const currentPath = window.location.pathname;
if (currentPath === '/' || currentPath.endsWith('/index.html') || currentPath.endsWith('/')) {
window.location.href = 'main/';
}
});
})();
8 changes: 8 additions & 0 deletions docs/source/_templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<title>Redirecting to the latest EmbodiChain documentation</title>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=./main/index.html">
</head>
Comment on lines +4 to +7
Comment on lines +4 to +7
</html>
Loading
Loading