Braids-docs/code/deploy-braids.py
2026-01-16 14:22:26 +01:00

156 lines
4.5 KiB
Python
Executable file

#!/usr/bin/env python3
# This script gets all the branches of the repo (set belwo) with a certain prefix
# and copies them to the webroot as separate directories.
# Not very secure ofcourse so only run the webhook when the workshop is ongoing
# and remove the files afterwards.
import os
import shutil
import subprocess
import tempfile
import fcntl
from pathlib import Path
REPO = Path("/srv/git/braids.git") # bare/mirror repo
WEBROOT = Path("/var/www/html/") # public root
PREFIX = "braids" # this sets both the prefix for the branches <prefix/name> as the directory the sites will be published in.
PEOPLE_DIR = WEBROOT / PREFIX
# Had to experiment with a few different location for this. /tmp and /run did not work.
LOCKFILE = Path("/var/lock/braids-deploy.lock")
REF_PREFIX = "refs/heads/" + PREFIX + "/"
ALLOWED_SLUG_CHARS = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-")
def run(cmd, cwd=None, input_bytes=None):
result = subprocess.run(
cmd,
cwd=str(cwd) if cwd else None,
input=input_bytes,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=True,
)
return result.stdout
def is_valid_slug(slug):
if not slug:
return False
for ch in slug:
if ch not in ALLOWED_SLUG_CHARS:
return False
return True
# Gets all branches for the prefix.
def get_people_refs():
out = run([
"git",
"-C",
str(REPO),
"for-each-ref",
"--format=%(refname)",
"refs/heads/" + PREFIX +"/*",
])
refs = []
for line in out.decode("utf-8", errors="replace").splitlines():
line = line.strip()
if not line.startswith(REF_PREFIX):
continue
slug = line[len(REF_PREFIX):]
if is_valid_slug(slug):
refs.append((line, slug))
refs.sort(key=lambda item: item[1].lower())
return refs
# create the directories if necessary
def ensure_dirs():
WEBROOT.mkdir(parents=True, exist_ok=True)
PEOPLE_DIR.mkdir(parents=True, exist_ok=True)
# update the local bare repo
def fetch_mirror():
run(["git", "-C", str(REPO), "fetch", "--prune", "origin", "+refs/heads/*:refs/heads/*"])
def publish_one(ref, slug):
tmpdir = Path(tempfile.mkdtemp(prefix="braids-" + slug + "-"))
try:
# Export branch root
archive = run(["git", "-C", str(REPO), "archive", ref])
run([
"tar",
"-x",
"-C",
str(tmpdir),
"--no-same-owner",
"--no-same-permissions",
], input_bytes=archive)
dest = PEOPLE_DIR / slug
if dest.exists():
shutil.rmtree(dest)
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(tmpdir), str(dest))
make_world_readable(dest)
except subprocess.CalledProcessError:
# If branch doesn't contain that folder, just skip it.
pass
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
def make_world_readable(path):
for root, dirs, files in os.walk(path):
root_path = Path(root)
for d in dirs:
os.chmod(root_path / d, 0o755)
for f in files:
os.chmod(root_path / f, 0o644)
os.chmod(path, 0o755)
# create a simple html file with links to the documents. iframes because it looks nice.
def rebuild_index(refs):
ensure_dirs()
parts = [
"<!doctype html><meta charset='utf-8'>",
"<title>Braids</title>",
"<style>",
"body{font-family:sans-serif;margin:20px}",
".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}",
".card{border:1px solid #ddd;padding:12px}",
"iframe{width:100%;height:260px;border:1px solid #eee}",
"</style>",
"<h1>Braids</h1>",
"<div class='grid'>",
]
for _, slug in refs:
parts.append("<div class='card'>")
parts.append(f"<div><a href='{PREFIX}/{slug}/' target='_blank'>{slug}</a></div>")
parts.append(f"<iframe src='{PREFIX}/{slug}/'></iframe>")
parts.append("</div>")
parts.append("</div>")
(WEBROOT / "index.html").write_text("\n".join(parts), encoding="utf-8")
def main():
# Lock (avoid concurrent deploys)
LOCKFILE.parent.mkdir(parents=True, exist_ok=True)
with open(LOCKFILE, "w") as lf:
fcntl.flock(lf, fcntl.LOCK_EX)
ensure_dirs()
fetch_mirror()
refs = get_people_refs()
for ref, slug in refs:
publish_one(ref, slug)
rebuild_index(refs)
if __name__ == "__main__":
main()