MimIR
0.4-dev
MimIR is my Intermediate Representation
Toggle main menu visibility
Loading...
Searching...
No Matches
deploy.py
Go to the documentation of this file.
1
#!/usr/bin/env python3
2
"""Deploy generated documentation into the website repository."""
3
4
import
argparse
5
import
json
6
import
os
7
import
re
8
import
shutil
9
import
subprocess
10
import
sys
11
from
pathlib
import
Path
12
13
14
VERSION_RE = re.compile(
r'v[0-9][A-Za-z0-9.+-]*'
)
15
16
17
def
parse_args
() -> argparse.Namespace:
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()
26
27
28
def
remove_path
(path: Path) ->
None
:
29
"""Remove a file or directory."""
30
if
path.is_dir()
and
not
path.is_symlink():
31
shutil.rmtree(path)
32
else
:
33
path.unlink()
34
35
36
def
copy_children
(source_dir: Path, target_dir: Path) ->
None
:
37
"""Copy all children from source_dir into target_dir."""
38
for
child
in
source_dir.iterdir():
39
destination = target_dir / child.name
40
if
child.is_dir():
41
shutil.copytree(child, destination)
42
else
:
43
shutil.copy2(child, destination)
44
45
46
def
deploy_root
(build_dir: Path, site_dir: Path) ->
None
:
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))
50
51
for
child
in
site_dir.iterdir():
52
if
child.name
not
in
preserved_names:
53
remove_path
(child)
54
55
copy_children
(build_dir, site_dir)
56
57
58
def
deploy_version
(build_dir: Path, site_dir: Path, version: str) ->
None
:
59
"""Deploy docs to a versioned subdirectory."""
60
target_dir = site_dir / version
61
if
target_dir.exists():
62
remove_path
(target_dir)
63
target_dir.mkdir(parents=
True
, exist_ok=
True
)
64
copy_children
(build_dir, target_dir)
65
66
67
def
natural_key
(name: str) -> list[object]:
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)]
70
71
72
def
update_versions
(site_dir: Path) ->
None
:
73
"""Update versions.json in the root docs and each versioned snapshot."""
74
version_dirs = [
75
path
for
path
in
site_dir.iterdir()
76
if
path.is_dir()
and
VERSION_RE.fullmatch(path.name)
and
(path /
'index.html'
).exists()
77
]
78
79
versions: list[dict[str, str]] = []
80
if
(site_dir /
'index.html'
).exists():
81
versions.append({
'label'
:
'master'
,
'href'
:
'/'
})
82
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}/'
})
85
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)
89
90
91
def
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(
94
[
'git'
, *args],
95
cwd=site_dir,
96
check=check,
97
capture_output=
True
,
98
text=
True
,
99
)
100
101
102
def
commit_and_push
(site_dir: Path, label: str) ->
None
:
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:
106
return
107
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}'
)
111
112
for
attempt
in
range(3):
113
if
git
(site_dir,
'push'
,
'origin'
,
'master'
, check=
False
).returncode == 0:
114
return
115
git
(site_dir,
'pull'
,
'--rebase'
,
'origin'
,
'master'
)
116
if
attempt < 2:
117
continue
118
119
raise
RuntimeError(
'failed to push deployed docs after 3 attempts'
)
120
121
122
def
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}'
)
128
if
not
git_ref:
129
raise
ValueError(
'Missing git ref'
)
130
if
not
ref_name:
131
raise
ValueError(
'Missing ref name'
)
132
133
if
git_ref ==
'refs/heads/master'
:
134
deploy_root
(build_dir, site_dir)
135
label =
'master'
136
else
:
137
deploy_version
(build_dir, site_dir, ref_name)
138
label = ref_name
139
140
update_versions
(site_dir)
141
(site_dir /
'.nojekyll'
).touch()
142
commit_and_push
(site_dir, label)
143
144
145
def
main
() -> int:
146
"""Program entry point."""
147
args =
parse_args
()
148
deploy
(args.build_dir.resolve(), args.site_dir.resolve(), args.git_ref, args.ref_name)
149
return
0
150
151
152
if
__name__ ==
'__main__'
:
153
sys.exit(
main
())
int
deploy
Definition
deploy.py:1
deploy.copy_children
None copy_children(Path source_dir, Path target_dir)
Definition
deploy.py:36
deploy.update_versions
None update_versions(Path site_dir)
Definition
deploy.py:72
deploy.main
int main()
Definition
deploy.py:145
deploy.deploy_root
None deploy_root(Path build_dir, Path site_dir)
Definition
deploy.py:46
deploy.remove_path
None remove_path(Path path)
Definition
deploy.py:28
deploy.deploy_version
None deploy_version(Path build_dir, Path site_dir, str version)
Definition
deploy.py:58
deploy.commit_and_push
None commit_and_push(Path site_dir, str label)
Definition
deploy.py:102
deploy.natural_key
list[object] natural_key(str name)
Definition
deploy.py:67
deploy.parse_args
argparse.Namespace parse_args()
Definition
deploy.py:17
deploy.git
subprocess.CompletedProcess[str] git(Path site_dir, *str args, bool check=True)
Definition
deploy.py:91
docs
deploy.py
Generated by
1.18.0