Skip to content

scheduled docs generation #105

scheduled docs generation

scheduled docs generation #105

Workflow file for this run

name: scheduled docs generation
on:
schedule:
- cron: '0 5 * * *'
workflow_dispatch:
push:
# Pages deployment requires pages:write and id-token:write.
# contents:write is no longer needed since we no longer push to gh-pages directly.
permissions:
contents: read
pages: write
id-token: write
# Prevent overlapping scheduled runs from deploying to GitHub Pages simultaneously.
concurrency:
group: pages-${{ github.ref }}
cancel-in-progress: false
jobs:
get-versions:
runs-on: ubuntu-latest
outputs:
versions: ${{ steps.get-versions.outputs.versions }}
steps:
- name: Get supported Python versions and translations
id: get-versions
run: |
python - <<'PY' >> "$GITHUB_OUTPUT"
import json
import tomllib
from urllib.request import urlopen
with urlopen("https://peps.python.org/api/release-cycle.json", timeout=30) as response:
release_cycle = json.load(response)
versions = [
{
"branch": release.get("branch") or version,
"python_version": "3.12" if version == "3.10" else "3",
}
for version, release in release_cycle.items()
if release.get("status") not in {"end-of-life", "planned"}
]
with urlopen(
"https://raw.githubusercontent.com/python/docsbuild-scripts/main/config.toml",
timeout=30,
) as response:
config = tomllib.loads(response.read().decode("utf-8"))
defaults = config.get("defaults", {})
default_in_prod = defaults.get("in_prod", True)
default_sphinxopts = defaults.get("sphinxopts", [])
languages = []
for language, language_config in config.get("languages", {}).items():
if not language_config.get("in_prod", default_in_prod):
continue
languages.append(
{
"language": language,
"sphinxopts_json": json.dumps(
language_config.get("sphinxopts", default_sphinxopts),
ensure_ascii=False,
),
}
)
matrix = [{**version, **language} for version in versions for language in languages]
print(f"versions={json.dumps(matrix, ensure_ascii=False)}")
PY
build:
needs: get-versions
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.get-versions.outputs.versions) }}
# Build jobs run concurrently and upload artifacts per version. The single
# deploy job below collects all artifacts and publishes once, avoiding
# concurrent git pushes/commit conflicts to gh-pages.
uses: ./.github/workflows/build.yaml
with:
reference: ${{ matrix.branch }}
python_version: ${{ matrix.python_version }}
language: ${{ matrix.language }}
sphinxopts_json: ${{ matrix.sphinxopts_json }}
publish: ${{ 'false' }}
deploy:
# A single deploy job runs after all build matrix jobs complete.
# Artifacts from every concurrent build job are merged here and published
# once via actions/deploy-pages, eliminating concurrent gh-pages push conflicts.
needs: build
if: ${{ !cancelled() && needs.build.result != 'failure' && github.event_name != 'push' }}
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Checkout existing gh-pages content
id: checkout-pages
uses: actions/checkout@v5
with:
ref: gh-pages
path: _site
continue-on-error: true
- name: Prepare site directory
run: |
# Remove git metadata; safe even if checkout above did not succeed
rm -rf _site/.git
- name: Download all build artifacts
# Collect artifacts uploaded by all concurrent build matrix jobs
uses: actions/download-artifact@v4
with:
path: artifacts/
- name: Copy new archives into site directory
run: |
# Copy generated archives (zip, tar.bz2, epub) into _site/<lang>/<major_minor>/,
# excluding PDF build logs which are for debugging only.
# Extract major.minor and language from filenames like:
# python-3.14.0-docs-html.zip (English zip)
# python-3.14.0-docs.epub (English epub)
# python-3.14.0-fr-docs-html.zip (French zip)
# python-3.14.0-fr-docs.epub (French epub)
python - <<'PY'
import re
import shutil
from pathlib import Path
artifacts = Path("artifacts")
site = Path("_site")
name_re = re.compile(
r"^python-(\d+\.\d+)[\d.]*(?:-(?!docs|pdf)([a-zA-Z][a-zA-Z0-9_]*))?-(?:docs|pdf)"
)
for f in artifacts.rglob("*"):
if not f.is_file():
continue
name = f.name
if not (name.endswith(".zip") or name.endswith(".tar.bz2") or name.endswith(".epub")):
continue
if re.search(r"-pdf-logs\.zip$", name):
continue
m = name_re.match(name)
if not m:
continue
major_minor = m.group(1)
lang = (m.group(2) or "en").replace("_", "-").lower()
dest = site / lang / major_minor
dest.mkdir(parents=True, exist_ok=True)
shutil.copy2(f, dest / name)
PY
- name: Symlink 3 to stable version
run: |
# Create a relative symlink _site/en/3 -> <stable> (e.g. 3 -> 3.14),
# pointing to the first "bugfix" (stable) version from release-cycle JSON.
stable=$(curl -sf https://peps.python.org/api/release-cycle.json | \
jq -r '[to_entries[] | select(.value.status == "bugfix")] | first | .key')
if [ -z "$stable" ]; then
echo "Error: no stable (bugfix) version found in release-cycle JSON" >&2
exit 1
fi
# Remove existing 3 directory or symlink before creating new symlink
rm -rf _site/en/3
ln -s "$stable" _site/en/3
- name: Symlink bare version paths to en/ for backwards compatibility
run: |
# Create _site/<major_minor> -> en/<major_minor> symlinks so that old
# URLs like /3.14/ continue to work after content moved to /en/3.14/.
python - <<'PY'
import re
from pathlib import Path
site = Path("_site")
en_dir = site / "en"
version_pattern = re.compile(r"^\d+\.\d+$")
for version_dir in en_dir.iterdir():
if not version_dir.is_dir() or not version_pattern.match(version_dir.name):
continue
link = site / version_dir.name
if not link.exists() and not link.is_symlink():
link.symlink_to(Path("en") / version_dir.name)
PY
- name: Generate per-version directory listing
run: |
python - <<'PY'
import re
from datetime import datetime, timezone
from html import escape
from pathlib import Path
from urllib.parse import quote
root = Path("_site")
version_pattern = re.compile(r"^\d+\.\d+$")
for lang_dir in sorted(p for p in root.iterdir() if p.is_dir()):
for version_dir in sorted(
p for p in lang_dir.iterdir() if p.is_dir() and version_pattern.match(p.name)
):
files = sorted(
p for p in version_dir.iterdir() if p.is_file() and p.name != "index.html"
)
rows = []
for file_path in files:
stat = file_path.stat()
timestamp = datetime.fromtimestamp(stat.st_mtime, timezone.utc).strftime(
"%Y-%m-%d %H:%M:%S UTC"
)
rows.append(
f'<tr><td><a href="{quote(file_path.name)}">{escape(file_path.name)}</a></td><td>{timestamp}</td><td>{stat.st_size}</td></tr>'
)
relative_path = f"/{version_dir.relative_to(root).as_posix()}/"
html = [
"<!doctype html>",
'<html lang="en">',
'<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Directory listing</title></head>',
"<body>",
f"<h1>Path: {escape(relative_path)}</h1>",
"<table>",
'<thead><tr><th scope="col">Filename</th><th scope="col">Timestamp (UTC)</th><th scope="col">Size (bytes)</th></tr></thead>',
"<tbody>",
*rows,
"</tbody>",
"</table>",
"</body>",
"</html>",
]
(version_dir / "index.html").write_text("\n".join(html) + "\n", encoding="utf-8")
PY
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: _site
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4