2"""Deploy generated documentation into the website repository."""
11from pathlib
import Path
14VERSION_RE = re.compile(
r'v[0-9][A-Za-z0-9.+-]*')
18 """Parse command line arguments."""
19 workspace = Path(os.environ.get(
'GITHUB_WORKSPACE',
'.')).resolve()
20 parser = argparse.ArgumentParser(description=
'Deploy generated docs to the site checkout.')
21 parser.add_argument(
'--build-dir', type=Path, default=workspace /
'build' /
'html', help=
'Generated HTML docs directory')
22 parser.add_argument(
'--site-dir', type=Path, default=workspace /
'site', help=
'Checked out site repository')
23 parser.add_argument(
'--git-ref', default=os.environ.get(
'GITHUB_REF'), help=
'Git ref being deployed')
24 parser.add_argument(
'--ref-name', default=os.environ.get(
'GITHUB_REF_NAME'), help=
'Short git ref name being deployed')
25 return parser.parse_args()
29 """Remove a file or directory."""
30 if path.is_dir()
and not path.is_symlink():
37 """Copy all children from source_dir into target_dir."""
38 for child
in source_dir.iterdir():
39 destination = target_dir / child.name
41 shutil.copytree(child, destination)
43 shutil.copy2(child, destination)
47 """Deploy docs to the site root while preserving versioned subdirectories."""
48 preserved_names = {
'.git'}
49 preserved_names.update(path.name
for path
in site_dir.iterdir()
if path.is_dir()
and VERSION_RE.fullmatch(path.name))
51 for child
in site_dir.iterdir():
52 if child.name
not in preserved_names:
59 """Deploy docs to a versioned subdirectory."""
60 target_dir = site_dir / version
61 if target_dir.exists():
63 target_dir.mkdir(parents=
True, exist_ok=
True)
68 """Split version names into comparable text and numeric chunks."""
69 return [
int(part)
if part.isdigit()
else part
for part
in re.findall(
r'\d+|[^\d]+', name)]
73 """Update versions.json in the root docs and each versioned snapshot."""
75 path
for path
in site_dir.iterdir()
76 if path.is_dir()
and VERSION_RE.fullmatch(path.name)
and (path /
'index.html').exists()
79 versions: list[dict[str, str]] = []
80 if (site_dir /
'index.html').exists():
81 versions.append({
'label':
'master',
'href':
'/'})
83 for path
in sorted(version_dirs, key=
lambda entry:
natural_key(entry.name), reverse=
True):
84 versions.append({
'label': path.name,
'href': f
'/{path.name}/'})
86 payload = json.dumps(versions, indent=2) +
'\n'
87 for output_dir
in [site_dir, *version_dirs]:
88 (output_dir /
'versions.json').write_text(payload)
91def git(site_dir: Path, *args: str, check: bool =
True) -> subprocess.CompletedProcess[str]:
92 """Run a git command in the site repository."""
93 return subprocess.run(
103 """Commit site changes and push with a small rebase retry loop."""
104 git(site_dir,
'add',
'-A')
105 if git(site_dir,
'diff',
'--cached',
'--quiet', check=
False).returncode == 0:
108 git(site_dir,
'config',
'user.name',
'github-actions[bot]')
109 git(site_dir,
'config',
'user.email',
'41898282+github-actions[bot]@users.noreply.github.com')
110 git(site_dir,
'commit',
'-m', f
'Deploy docs for {label}')
112 for attempt
in range(3):
113 if git(site_dir,
'push',
'origin',
'master', check=
False).returncode == 0:
115 git(site_dir,
'pull',
'--rebase',
'origin',
'master')
119 raise RuntimeError(
'failed to push deployed docs after 3 attempts')
122def deploy(build_dir: Path, site_dir: Path, git_ref: str, ref_name: str) ->
None:
123 """Deploy docs to the correct location based on the ref."""
124 if not build_dir.is_dir():
125 raise FileNotFoundError(f
'Build directory does not exist: {build_dir}')
126 if not site_dir.is_dir():
127 raise FileNotFoundError(f
'Site directory does not exist: {site_dir}')
129 raise ValueError(
'Missing git ref')
131 raise ValueError(
'Missing ref name')
133 if git_ref ==
'refs/heads/master':
141 (site_dir /
'.nojekyll').touch()
146 """Program entry point."""
148 deploy(args.build_dir.resolve(), args.site_dir.resolve(), args.git_ref, args.ref_name)
152if __name__ ==
'__main__':
list[object] natural_key(str name)
None copy_children(Path source_dir, Path target_dir)
None deploy_root(Path build_dir, Path site_dir)
None remove_path(Path path)
subprocess.CompletedProcess[str] git(Path site_dir, *str args, bool check=True)
None update_versions(Path site_dir)
argparse.Namespace parse_args()
None commit_and_push(Path site_dir, str label)
None deploy_version(Path build_dir, Path site_dir, str version)