Skip to content

Commit 2ea1381

Browse files
committed
Implement a simple github webook server to build the doc.
1 parent 18a694a commit 2ea1381

File tree

4 files changed

+214
-1
lines changed

4 files changed

+214
-1
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,15 @@ of Sphinx we're using where::
5050
3.10 2.3.1 2.3.1 2.3.1 2.3.1 2.3.1 2.3.1 2.3.1 2.3.1 2.3.1
5151
3.11 2.3.1 2.3.1 2.3.1 2.3.1 2.3.1 2.3.1 2.3.1 2.3.1 2.3.1
5252
======== ======= ===== ======= ===== ===== ===== ======= ===== =====
53+
54+
55+
## The github hook server
56+
57+
`build_docs_server.py` is a simple HTTP server handling Github Webhooks
58+
requests to build the doc when needed. It only needs `push` events.
59+
60+
Its logging can be configured by giving a yaml file path to the
61+
`--logging-config` argument.
62+
63+
By default the loglevel is `DEBUG` on `stderr`, the default config can
64+
be found in the code so one can bootstrap a different config from it.

build_docs_server.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
"""Github hook server.
2+
3+
This is a simple HTTP server handling Github Webhooks requests to
4+
build the doc when needed.
5+
6+
It needs a GH_SECRET environment variable to be able to receive hooks
7+
on `/hook/github`.
8+
9+
Its logging can be configured by giving a yaml file path to the
10+
`--logging-config` argument.
11+
12+
By default the loglevel is `DEBUG` on `stderr`, the default config can
13+
be found in the code so one can bootstrap a different config from it.
14+
"""
15+
16+
from pathlib import Path
17+
import argparse
18+
import asyncio
19+
import logging.config
20+
import os
21+
22+
from aiohttp import web
23+
from gidgethub import sansio
24+
import yaml
25+
26+
__version__ = "0.0.1"
27+
28+
DEFAULT_LOGGING_CONFIG = """
29+
---
30+
31+
version: 1
32+
disable_existing_loggers: false
33+
formatters:
34+
normal:
35+
format: '%(asctime)s - %(levelname)s - %(message)s'
36+
handlers:
37+
stderr:
38+
class: logging.StreamHandler
39+
stream: ext://sys.stderr
40+
level: DEBUG
41+
formatter: normal
42+
loggers:
43+
build_docs_server:
44+
level: DEBUG
45+
handlers: [stderr]
46+
aiohttp.access:
47+
level: DEBUG
48+
handlers: [stderr]
49+
aiohttp.client:
50+
level: DEBUG
51+
handlers: [stderr]
52+
aiohttp.internal:
53+
level: DEBUG
54+
handlers: [stderr]
55+
aiohttp.server:
56+
level: DEBUG
57+
handlers: [stderr]
58+
aiohttp.web:
59+
level: DEBUG
60+
handlers: [stderr]
61+
aiohttp.websocket:
62+
level: DEBUG
63+
handlers: [stderr]
64+
"""
65+
66+
logger = logging.getLogger("build_docs_server")
67+
68+
69+
async def version(request):
70+
return web.json_response(
71+
{
72+
"name": "docs.python.org Github handler",
73+
"version": __version__,
74+
"source": "https://github.com/python/docsbuild-scripts",
75+
}
76+
)
77+
78+
79+
async def child_waiter(app):
80+
while True:
81+
try:
82+
status = os.waitid(os.P_ALL, 0, os.WNOHANG | os.WEXITED)
83+
logger.debug("Child completed with status %s", str(status))
84+
except ChildProcessError:
85+
await asyncio.sleep(600)
86+
87+
88+
async def start_child_waiter(app):
89+
app["child_waiter"] = asyncio.ensure_future(child_waiter(app))
90+
91+
92+
async def stop_child_waiter(app):
93+
app["child_waiter"].cancel()
94+
95+
96+
async def hook(request):
97+
body = await request.read()
98+
event = sansio.Event.from_http(
99+
request.headers, body, secret=os.environ.get("GH_SECRET")
100+
)
101+
if event.event != "push":
102+
logger.debug(
103+
"Received a %s event, nothing to do.", event.event
104+
)
105+
return web.Response()
106+
touched_files = (
107+
set(event.data["head_commit"]["added"])
108+
| set(event.data["head_commit"]["modified"])
109+
| set(event.data["head_commit"]["removed"])
110+
)
111+
if not any("Doc" in touched_file for touched_file in touched_files):
112+
logger.debug("No documentation file modified, ignoring.")
113+
return web.Response() # Nothing to do
114+
branch = event.data["ref"].split("/")[-1]
115+
logger.debug("Forking a build for branch %s", branch)
116+
pid = os.fork()
117+
if pid == 0:
118+
os.execl(
119+
"/usr/bin/env",
120+
"/usr/bin/env",
121+
"python",
122+
"build_docs.py",
123+
"--branch",
124+
branch,
125+
)
126+
else:
127+
return web.Response()
128+
129+
130+
def parse_args():
131+
parser = argparse.ArgumentParser(description=__doc__)
132+
parser.add_argument("--path", help="Unix socket to listen for connections.")
133+
parser.add_argument("--port", help="Local port to listen for connections.")
134+
parser.add_argument(
135+
"--logging-config",
136+
help="yml file containing a Python logging dictconfig, see README.md",
137+
)
138+
return parser.parse_args()
139+
140+
141+
def main():
142+
args = parse_args()
143+
logging.config.dictConfig(
144+
yaml.load(
145+
Path(args.logging_config).read_text()
146+
if args.logging_config
147+
else DEFAULT_LOGGING_CONFIG,
148+
Loader=yaml.SafeLoader,
149+
)
150+
)
151+
app = web.Application()
152+
app.on_startup.append(start_child_waiter)
153+
app.on_cleanup.append(stop_child_waiter)
154+
app.add_routes(
155+
[
156+
web.get("/", version),
157+
web.post("/hooks/github", hook),
158+
]
159+
)
160+
web.run_app(app, path=args.path, port=args.port)
161+
162+
163+
if __name__ == "__main__":
164+
main()

requirements.in

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1-
sentry-sdk
1+
aiohttp
2+
gidgethub
23
jinja2
4+
pyyaml
5+
sentry-sdk
36
zc.lockfile

requirements.txt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,50 @@
44
#
55
# pip-compile requirements.in
66
#
7+
aiohttp==3.7.3
8+
# via -r requirements.in
9+
async-timeout==3.0.1
10+
# via aiohttp
11+
attrs==20.3.0
12+
# via aiohttp
713
certifi==2020.6.20
814
# via sentry-sdk
15+
cffi==1.14.4
16+
# via cryptography
17+
chardet==3.0.4
18+
# via aiohttp
19+
cryptography==3.3.1
20+
# via pyjwt
21+
gidgethub==4.2.0
22+
# via -r requirements.in
23+
idna==2.10
24+
# via yarl
925
jinja2==2.11.2
1026
# via -r requirements.in
1127
markupsafe==1.1.1
1228
# via jinja2
29+
multidict==5.1.0
30+
# via
31+
# aiohttp
32+
# yarl
33+
pycparser==2.20
34+
# via cffi
35+
pyjwt[crypto]==1.7.1
36+
# via gidgethub
37+
pyyaml==5.3.1
38+
# via -r requirements.in
1339
sentry-sdk==0.15.1
1440
# via -r requirements.in
41+
six==1.15.0
42+
# via cryptography
43+
typing-extensions==3.7.4.3
44+
# via aiohttp
45+
uritemplate==3.0.1
46+
# via gidgethub
1547
urllib3==1.25.9
1648
# via sentry-sdk
49+
yarl==1.6.3
50+
# via aiohttp
1751
zc.lockfile==2.0
1852
# via -r requirements.in
1953

0 commit comments

Comments
 (0)