MimIR
0.5-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
# Deployed by other workflows; a docs deploy must not sweep them away.
17
FOREIGN_DIRS = {
'playground'
}
18
19
20
def
parse_args
() -> argparse.Namespace:
21
"""Parse command line arguments."""
22
workspace = Path(os.environ.get(
'GITHUB_WORKSPACE'
,
'.'
)).resolve()
23
parser = argparse.ArgumentParser(description=
'Deploy generated docs to the site checkout.'
)
24
parser.add_argument(
'--build-dir'
, type=Path, default=workspace /
'build'
/
'html'
, help=
'Generated HTML docs directory'
)
25
parser.add_argument(
'--site-dir'
, type=Path, default=workspace /
'site'
, help=
'Checked out site repository'
)
26
parser.add_argument(
'--git-ref'
, default=os.environ.get(
'GITHUB_REF'
), help=
'Git ref being deployed'
)
27
parser.add_argument(
'--ref-name'
, default=os.environ.get(
'GITHUB_REF_NAME'
), help=
'Short git ref name being deployed'
)
28
parser.add_argument(
'--subdir'
, choices=sorted(FOREIGN_DIRS),
29
help=
'Deploy into this subdirectory instead of the versioned docs layout'
)
30
return
parser.parse_args()
31
32
33
def
remove_path
(path: Path) ->
None
:
34
"""Remove a file or directory."""
35
if
path.is_dir()
and
not
path.is_symlink():
36
shutil.rmtree(path)
37
else
:
38
path.unlink()
39
40
41
def
copy_children
(source_dir: Path, target_dir: Path) ->
None
:
42
"""Copy all children from source_dir into target_dir."""
43
for
child
in
source_dir.iterdir():
44
destination = target_dir / child.name
45
if
child.is_dir():
46
shutil.copytree(child, destination)
47
else
:
48
shutil.copy2(child, destination)
49
50
51
def
deploy_root
(build_dir: Path, site_dir: Path) ->
None
:
52
"""Deploy docs to the site root while preserving versioned subdirectories."""
53
preserved_names = {
'.git'
, *FOREIGN_DIRS}
54
preserved_names.update(path.name
for
path
in
site_dir.iterdir()
if
path.is_dir()
and
VERSION_RE.fullmatch(path.name))
55
56
for
child
in
site_dir.iterdir():
57
if
child.name
not
in
preserved_names:
58
remove_path
(child)
59
60
copy_children
(build_dir, site_dir)
61
62
63
def
deploy_subdir
(build_dir: Path, site_dir: Path, name: str) ->
None
:
64
"""Deploy to a subdirectory of the site, replacing whatever is there."""
65
target_dir = site_dir / name
66
if
target_dir.exists():
67
remove_path
(target_dir)
68
target_dir.mkdir(parents=
True
, exist_ok=
True
)
69
copy_children
(build_dir, target_dir)
70
71
72
def
natural_key
(name: str) -> list[object]:
73
"""Split version names into comparable text and numeric chunks."""
74
return
[
int
(part)
if
part.isdigit()
else
part
for
part
in
re.findall(
r'\d+|[^\d]+'
, name)]
75
76
77
def
update_versions
(site_dir: Path) ->
None
:
78
"""Update versions.json in the root docs and each versioned snapshot."""
79
version_dirs = [
80
path
for
path
in
site_dir.iterdir()
81
if
path.is_dir()
and
VERSION_RE.fullmatch(path.name)
and
(path /
'index.html'
).exists()
82
]
83
84
versions: list[dict[str, str]] = []
85
if
(site_dir /
'index.html'
).exists():
86
versions.append({
'label'
:
'master'
,
'href'
:
'/'
})
87
88
for
path
in
sorted(version_dirs, key=
lambda
entry:
natural_key
(entry.name), reverse=
True
):
89
versions.append({
'label'
: path.name,
'href'
: f
'/{path.name}/'
})
90
91
payload = json.dumps(versions, indent=2) +
'\n'
92
for
output_dir
in
[site_dir, *version_dirs]:
93
(output_dir /
'versions.json'
).write_text(payload)
94
95
96
def
git
(site_dir: Path, *args: str, check: bool =
True
) -> subprocess.CompletedProcess[str]:
97
"""Run a git command in the site repository."""
98
return
subprocess.run(
99
[
'git'
, *args],
100
cwd=site_dir,
101
check=check,
102
capture_output=
True
,
103
text=
True
,
104
)
105
106
107
def
commit_and_push
(site_dir: Path, what: str) ->
None
:
108
"""Commit site changes and push with a small rebase retry loop."""
109
git
(site_dir,
'add'
,
'-A'
)
110
if
git
(site_dir,
'diff'
,
'--cached'
,
'--quiet'
, check=
False
).returncode == 0:
111
return
112
113
git
(site_dir,
'config'
,
'user.name'
,
'github-actions[bot]'
)
114
git
(site_dir,
'config'
,
'user.email'
,
'41898282+github-actions[bot]@users.noreply.github.com'
)
115
git
(site_dir,
'commit'
,
'-m'
, f
'Deploy {what}'
)
116
117
for
attempt
in
range(3):
118
if
git
(site_dir,
'push'
,
'origin'
,
'master'
, check=
False
).returncode == 0:
119
return
120
if
attempt < 2:
121
r =
git
(site_dir,
'pull'
,
'--rebase'
,
'origin'
,
'master'
, check=
False
)
122
if
r.returncode != 0:
123
raise
RuntimeError(f
'rebase failed:\n{r.stdout}{r.stderr}'
)
124
125
raise
RuntimeError(
'failed to push deployed docs after 3 attempts'
)
126
127
128
def
deploy
(build_dir: Path, site_dir: Path, git_ref: str, ref_name: str, subdir: str |
None
) ->
None
:
129
"""Deploy docs to the correct location based on the ref."""
130
if
not
build_dir.is_dir():
131
raise
FileNotFoundError(f
'Build directory does not exist: {build_dir}'
)
132
if
not
site_dir.is_dir():
133
raise
FileNotFoundError(f
'Site directory does not exist: {site_dir}'
)
134
if
not
git_ref:
135
raise
ValueError(
'Missing git ref'
)
136
if
not
ref_name:
137
raise
ValueError(
'Missing ref name'
)
138
139
if
subdir:
140
deploy_subdir
(build_dir, site_dir, subdir)
141
what = f
'{subdir} for {ref_name}'
142
else
:
143
if
git_ref ==
'refs/heads/master'
:
144
deploy_root
(build_dir, site_dir)
145
label =
'master'
146
else
:
147
deploy_subdir
(build_dir, site_dir, ref_name)
148
label = ref_name
149
update_versions
(site_dir)
150
what = f
'docs for {label}'
151
152
(site_dir /
'.nojekyll'
).touch()
153
commit_and_push
(site_dir, what)
154
155
156
def
main
() -> int:
157
"""Program entry point."""
158
args =
parse_args
()
159
deploy
(args.build_dir.resolve(), args.site_dir.resolve(), args.git_ref, args.ref_name, args.subdir)
160
return
0
161
162
163
if
__name__ ==
'__main__'
:
164
sys.exit(
main
())
int
deploy
Definition
deploy.py:1
deploy.deploy_subdir
None deploy_subdir(Path build_dir, Path site_dir, str name)
Definition
deploy.py:63
deploy.copy_children
None copy_children(Path source_dir, Path target_dir)
Definition
deploy.py:41
deploy.update_versions
None update_versions(Path site_dir)
Definition
deploy.py:77
deploy.main
int main()
Definition
deploy.py:156
deploy.deploy_root
None deploy_root(Path build_dir, Path site_dir)
Definition
deploy.py:51
deploy.remove_path
None remove_path(Path path)
Definition
deploy.py:33
deploy.natural_key
list[object] natural_key(str name)
Definition
deploy.py:72
deploy.parse_args
argparse.Namespace parse_args()
Definition
deploy.py:20
deploy.git
subprocess.CompletedProcess[str] git(Path site_dir, *str args, bool check=True)
Definition
deploy.py:96
deploy.commit_and_push
None commit_and_push(Path site_dir, str what)
Definition
deploy.py:107
docs
deploy.py
Generated by
1.18.0