Skip to content

Commit 0843640

Browse files
Copilotwannaphong
andauthored
Automate versioned docs publishing to PyThaiNLP/docs on release (#1338)
* Initial plan * feat: add publish-versioned-docs workflow and update_docs_index script Co-authored-by: wannaphong <8536487+wannaphong@users.noreply.github.com> * fix: add explicit GITHUB_TOKEN permissions to publish-versioned-docs workflow Co-authored-by: wannaphong <8536487+wannaphong@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: wannaphong <8536487+wannaphong@users.noreply.github.com>
1 parent a32ad38 commit 0843640

2 files changed

Lines changed: 311 additions & 0 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# SPDX-FileCopyrightText: 2026 PyThaiNLP Project
2+
# SPDX-FileType: SOURCE
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
# Publish versioned documentation to PyThaiNLP/docs when a release is published.
6+
#
7+
# On each GitHub release:
8+
# 1. Copy PyThaiNLP/dev-docs to PyThaiNLP/docs/{version}/.
9+
# 2. Regenerate PyThaiNLP/docs/index.html to list all released versions.
10+
# 3. Inject the new version entry into PyThaiNLP/dev-docs/index.html.
11+
12+
name: Publish versioned docs
13+
14+
on:
15+
release:
16+
types: [published]
17+
18+
jobs:
19+
publish-versioned-docs:
20+
name: Publish versioned documentation
21+
runs-on: ubuntu-latest
22+
# Only run for the main repository, not forks.
23+
if: github.repository == 'PyThaiNLP/pythainlp'
24+
permissions:
25+
contents: read
26+
27+
steps:
28+
- name: Get version from release tag
29+
id: version
30+
# Strip the leading 'v' from the tag name, e.g. 'v5.3.1' -> '5.3.1'.
31+
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
32+
33+
- name: Checkout pythainlp (for scripts)
34+
uses: actions/checkout@v6
35+
with:
36+
path: pythainlp
37+
38+
- name: Checkout dev-docs repository
39+
uses: actions/checkout@v6
40+
with:
41+
repository: PyThaiNLP/dev-docs
42+
token: ${{ secrets.PERSONAL_TOKEN }}
43+
path: dev-docs
44+
45+
- name: Checkout docs repository
46+
uses: actions/checkout@v6
47+
with:
48+
repository: PyThaiNLP/docs
49+
token: ${{ secrets.PERSONAL_TOKEN }}
50+
path: docs
51+
52+
- name: Set up Python
53+
uses: actions/setup-python@v6
54+
with:
55+
python-version: "3.12"
56+
57+
- name: Copy dev-docs to versioned directory in docs
58+
run: |
59+
VERSION="${{ steps.version.outputs.version }}"
60+
mkdir -p "docs/${VERSION}"
61+
rsync -a --delete --exclude='.git' dev-docs/ "docs/${VERSION}/"
62+
63+
- name: Update docs index and dev-docs version list
64+
run: |
65+
python pythainlp/build_tools/github/update_docs_index.py \
66+
--docs-dir docs \
67+
--dev-docs-index dev-docs/index.html \
68+
--version "${{ steps.version.outputs.version }}"
69+
70+
- name: Commit and push updated docs
71+
run: |
72+
VERSION="${{ steps.version.outputs.version }}"
73+
cd docs
74+
git config user.name 'github-actions[bot]'
75+
git config user.email 'github-actions[bot]@users.noreply.github.com'
76+
git add .
77+
git diff --staged --quiet || \
78+
git commit -m "Add docs for version ${VERSION}"
79+
git push
80+
81+
- name: Commit and push updated dev-docs index
82+
run: |
83+
VERSION="${{ steps.version.outputs.version }}"
84+
cd dev-docs
85+
git config user.name 'github-actions[bot]'
86+
git config user.email 'github-actions[bot]@users.noreply.github.com'
87+
git add index.html
88+
git diff --staged --quiet || \
89+
git commit -m "Update version list for release ${VERSION}"
90+
git push
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# SPDX-FileCopyrightText: 2026 PyThaiNLP Project
2+
# SPDX-FileType: SOURCE
3+
# SPDX-License-Identifier: Apache-2.0
4+
"""
5+
Update documentation index pages on release.
6+
7+
This script performs two tasks:
8+
9+
1. Generates or updates ``index.html`` in the versioned docs repository
10+
(``PyThaiNLP/docs``) to list all released versions as a landing page.
11+
2. Injects or updates a released-versions section in ``index.html`` of the
12+
development docs repository (``PyThaiNLP/dev-docs``).
13+
14+
Usage::
15+
16+
python update_docs_index.py \\
17+
--docs-dir /path/to/docs \\
18+
--dev-docs-index /path/to/dev-docs/index.html \\
19+
--version 5.3.1
20+
"""
21+
22+
from __future__ import annotations
23+
24+
import argparse
25+
import re
26+
import sys
27+
from pathlib import Path
28+
29+
# Markers used to delimit the injected versions section in dev-docs/index.html.
30+
_VERSIONS_BEGIN: str = "<!-- BEGIN VERSIONS -->"
31+
_VERSIONS_END: str = "<!-- END VERSIONS -->"
32+
33+
_VERSIONS_SECTION_TMPL: str = """\
34+
<!-- BEGIN VERSIONS -->
35+
<section id="released-versions">
36+
<h2>Released versions</h2>
37+
<ul>
38+
{items}
39+
</ul>
40+
</section>
41+
<!-- END VERSIONS -->"""
42+
43+
_VERSION_ITEM_TMPL: str = (
44+
' <li><a href="https://pythainlp.github.io/docs/{v}/">{v}</a></li>'
45+
)
46+
47+
_DOCS_INDEX_TMPL: str = """\
48+
<!DOCTYPE html>
49+
<html lang="en">
50+
<head>
51+
<meta charset="UTF-8">
52+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
53+
<title>PyThaiNLP Documentation</title>
54+
<style>
55+
body {{
56+
font-family: sans-serif;
57+
max-width: 800px;
58+
margin: 2rem auto;
59+
padding: 0 1rem;
60+
}}
61+
h1 {{ color: #333; }}
62+
ul {{ list-style: none; padding: 0; }}
63+
li {{ margin: 0.5rem 0; }}
64+
a {{ color: #0066cc; text-decoration: none; }}
65+
a:hover {{ text-decoration: underline; }}
66+
</style>
67+
</head>
68+
<body>
69+
<h1>PyThaiNLP Documentation</h1>
70+
<p>
71+
Latest development docs:
72+
<a href="https://pythainlp.github.io/dev-docs/">dev-docs</a>
73+
</p>
74+
<h2>Released versions</h2>
75+
<ul>
76+
{items}
77+
</ul>
78+
</body>
79+
</html>
80+
"""
81+
82+
83+
def _is_version_dir(name: str) -> bool:
84+
"""Return True if *name* looks like a version directory (e.g. ``5.3.1``)."""
85+
return bool(re.match(r"^\d+\.\d+", name))
86+
87+
88+
def _version_key(version: str) -> tuple:
89+
"""Return a sort key for a version string."""
90+
parts = re.split(r"[.\-]", version)
91+
key: list[int] = []
92+
for part in parts:
93+
try:
94+
key.append(int(part))
95+
except ValueError:
96+
break
97+
return tuple(key)
98+
99+
100+
def _get_version_dirs(docs_dir: Path) -> list[str]:
101+
"""Return version directories in *docs_dir*, sorted newest-first."""
102+
versions = [
103+
p.name
104+
for p in docs_dir.iterdir()
105+
if p.is_dir() and _is_version_dir(p.name)
106+
]
107+
versions.sort(key=_version_key, reverse=True)
108+
return versions
109+
110+
111+
def update_docs_index(docs_dir: Path) -> None:
112+
"""Generate or overwrite ``docs_dir/index.html`` listing all versions.
113+
114+
:param docs_dir: Path to the versioned documentation repository root.
115+
"""
116+
versions = _get_version_dirs(docs_dir)
117+
items = "\n".join(
118+
f' <li><a href="{v}/">{v}</a></li>' for v in versions
119+
)
120+
content = _DOCS_INDEX_TMPL.format(items=items)
121+
index_path = docs_dir / "index.html"
122+
index_path.write_text(content, encoding="utf-8")
123+
print(f"Updated {index_path}")
124+
125+
126+
def update_dev_docs_index(index_path: Path, version: str) -> None:
127+
"""Inject or update a released-versions section in *index_path*.
128+
129+
Looks for ``<!-- BEGIN VERSIONS -->`` / ``<!-- END VERSIONS -->`` markers.
130+
If found, prepends *version* to the existing list (skips if already there).
131+
If not found, appends a new section before ``</body>`` (or at end of file).
132+
133+
:param index_path: Path to ``dev-docs/index.html``.
134+
:param version: Version string without leading ``v`` (e.g. ``5.3.1``).
135+
"""
136+
content = index_path.read_text(encoding="utf-8")
137+
new_item = _VERSION_ITEM_TMPL.format(v=version)
138+
139+
if _VERSIONS_BEGIN in content and _VERSIONS_END in content:
140+
begin = content.index(_VERSIONS_BEGIN)
141+
end = content.index(_VERSIONS_END) + len(_VERSIONS_END)
142+
section = content[begin:end]
143+
144+
# Skip if this version is already listed.
145+
if f"/{version}/" in section:
146+
print(f"{index_path}: version {version} already present, skipping.")
147+
return
148+
149+
# Prepend the new item inside the existing <ul> or <ol>.
150+
section = re.sub(
151+
r"(<[uo]l[^>]*>)",
152+
rf"\1\n{new_item}",
153+
section,
154+
count=1,
155+
)
156+
content = content[:begin] + section + content[end:]
157+
else:
158+
# No markers yet — append a new section.
159+
new_section = _VERSIONS_SECTION_TMPL.format(items=new_item)
160+
if "</body>" in content:
161+
content = content.replace("</body>", f"\n{new_section}\n</body>")
162+
else:
163+
content = content + f"\n{new_section}\n"
164+
165+
index_path.write_text(content, encoding="utf-8")
166+
print(f"Updated {index_path}")
167+
168+
169+
def main() -> None:
170+
"""Entry point."""
171+
parser = argparse.ArgumentParser(
172+
description="Update PyThaiNLP documentation index pages on release.",
173+
)
174+
parser.add_argument(
175+
"--docs-dir",
176+
type=Path,
177+
help=(
178+
"Path to the versioned docs repository. "
179+
"Generates or overwrites index.html listing all version directories."
180+
),
181+
)
182+
parser.add_argument(
183+
"--dev-docs-index",
184+
type=Path,
185+
help=(
186+
"Path to dev-docs/index.html. "
187+
"Injects or updates a released-versions section."
188+
),
189+
)
190+
parser.add_argument(
191+
"--version",
192+
required=True,
193+
help="Version string without leading 'v' (e.g. 5.3.1).",
194+
)
195+
args = parser.parse_args()
196+
197+
if not args.docs_dir and not args.dev_docs_index:
198+
parser.error("At least one of --docs-dir or --dev-docs-index is required.")
199+
200+
if args.docs_dir:
201+
if not args.docs_dir.is_dir():
202+
print(
203+
f"Error: --docs-dir '{args.docs_dir}' is not a directory.",
204+
file=sys.stderr,
205+
)
206+
sys.exit(1)
207+
update_docs_index(args.docs_dir)
208+
209+
if args.dev_docs_index:
210+
if not args.dev_docs_index.exists():
211+
print(
212+
f"Warning: --dev-docs-index '{args.dev_docs_index}' does not exist, "
213+
"skipping.",
214+
file=sys.stderr,
215+
)
216+
else:
217+
update_dev_docs_index(args.dev_docs_index, args.version)
218+
219+
220+
if __name__ == "__main__":
221+
main()

0 commit comments

Comments
 (0)