Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add preview site build script and GitHub Actions workflow #748

Merged
merged 3 commits into from
Jul 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/build-preview-site.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Build pelican-themes preview site

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write

# Allow one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: true

on:
# Triggers the workflow on push or pull request events but only for the "main" branch
push:
branches: [ "master" ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

steps:
- name: Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 1
submodules: recursive
- name: Checkout pelican
uses: actions/checkout@v3
with:
repository: getpelican/pelican
path: _pelican
fetch-depth: 1
- name: Setup python-3.10
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Cache Playwright browsers
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright/
key: ${{ runner.os }}-browsers
- name: Install pelican and shot-scraper
run: pip install pelican[markdown] shot-scraper
- name: Setup shot-scraper
run: shot-scraper install
- name: Generate output
run: python build-theme-previews.py

# Rsync to server hosting pelicanthemes.com
- name: Install SSH key
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_PRIVATE_KEY }}
known_hosts: unnecessary
- name: Adding known hosts
run: ssh-keyscan -p 22 -H pelicanthemes.com >> ~/.ssh/known_hosts
- name: Deploy with rsync
run: rsync -avz ./_output/ [email protected]:~/roles/caddy/sites/pelicanthemes.com/

# Deploy to Github Pages
- name: Setup Pages
uses: actions/configure-pages@v2
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: '_output'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ Thumbs.db
*~
.swp
.*.swp

# Preview artifacts #
#####################
_pelican
_output
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,6 @@
[submodule "alchemy"]
path = alchemy
url = https://github.com/nairobilug/pelican-alchemy.git
[submodule "Nuja"]
path = Nuja
url = https://github.com/allenskd/Nuja.git
[submodule "pjport"]
path = pjport
url = https://github.com/xm3ron/pjport.git
Expand Down
1 change: 0 additions & 1 deletion Nuja
Submodule Nuja deleted from a58fd6
237 changes: 237 additions & 0 deletions build-theme-previews.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import argparse
import logging
import subprocess
import os

from rich.logging import RichHandler
from rich.console import Console


FORMAT = "%(message)s"
logging.basicConfig(
level="NOTSET",
format=FORMAT,
datefmt="[%X]",
handlers=[RichHandler(show_path=False, console=Console(force_terminal=True))]
)
logger = logging.getLogger()

HTML_HEADER = """\
<!DOCTYPE html>
<html>
<head>
<style>

h1 {
margin 20px auto;
text-align: center;
}

ul {
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}

li {
width: 500px;
min-width: fit-content;
border: 1px solid gray;
background-color: whitesmoke;
border-radius: 5px;
margin: 25px 50px;
text-align: center;
}

li:hover {
background-color: lightgray;
}

a {
display: block;
text-decoration: none;
color: black;
font-size: 1.5em;
}

img {
max-width:450px;
border-radius: 5px;
margin: 25px auto;
border: 1px solid black;
}

footer {
margin: 20px auto;
text-align: center;
font-size: 1.1em;
}

footer a {
display: inline;
font-size: 1em;
}

footer a.success {
color: green;
}

footer a.fail {
color: red;
}
</style>
</head>
<body>
<h1>pelican-themes Preview</h1>
<ul>"""

HTML_FOOTER = """\
</ul>
<footer>
Successfully built <a href="index.html" class="success">{success} themes</a><br/>
Failed to build <a href="failed.html" class="fail">{fail} themes</a>
</footer>
</body>
</html>
"""


def setup_folders(args):
theme_root = os.path.abspath(os.path.dirname(__file__))
output_root = os.path.abspath(os.path.join(theme_root, args.output))
samples_root = os.path.abspath(os.path.join(theme_root, args.samples))
screenshot_root = os.path.abspath(os.path.join(output_root, "_screenshots"))

# requires `getpelican/pelican` cloned in `_pelican` folder
if os.path.exists(samples_root):
os.makedirs(os.path.join(samples_root, "content", "images"), exist_ok=True) # silence warning
else:
raise RuntimeError(
f"Samples folder does not exist: {samples_root}. "
"You can use `samples` from pelican by cloning it to `_pelican` folder"
)
# create output and screenshot folders
os.makedirs(output_root, exist_ok=True)
os.makedirs(screenshot_root, exist_ok=True)

return theme_root, samples_root, output_root, screenshot_root


def build_theme_previews(theme_root, samples_root, output_root, screenshot_root):
themes = [item for item in os.listdir(theme_root) if os.path.isdir(item) and not item.startswith((".", "_"))]
logger.info(f"processing {len(themes)} themes...")

# launch web server for taking screenshots
server = subprocess.Popen(
["python", "-m", "http.server", "-d", output_root],
stdout=subprocess.PIPE, stderr=subprocess.PIPE
)

fail = {}
success = {}
screenshot_processes = []

for theme in sorted(themes, key=lambda x: x.lower()):
theme_path = os.path.join(theme_root, theme)
if os.path.exists(os.path.join(theme_path, theme, "templates")):
# actual theme is in a subfolder
theme_path = os.path.join(theme_path, theme)
output_path = os.path.join(output_root, theme)
try:
process = subprocess.run([
"pelican",
os.path.join(samples_root, "content"),
"--settings", os.path.join(samples_root, "pelican.conf.py"),
"--extra-settings", f"SITENAME=\"{theme} preview\"",
"--relative-urls",
"--theme-path", theme_path,
"--output", output_path,
"--ignore-cache",
"--delete-output-directory"
],
check=True, capture_output=True, universal_newlines=True)
except subprocess.CalledProcessError as exc:
logger.error(f"[red]failed to generate : {theme}[/]", extra={"markup": True})
fail[theme] = exc.stdout
continue
success[theme] = output_path
screenshot_path = os.path.join(screenshot_root, f"{theme}.png")
screenshot_processes.append(
subprocess.Popen(
["shot-scraper", f"http://localhost:8000/{theme}", "-o", screenshot_path, "-w", "1280", "-h", "780", "--wait", "1000"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
)
logger.info(f"[green]successfully generated : {theme}[/]", extra={"markup": True})

# cleanup
logger.info("finalizing screenshots...")
for process in screenshot_processes:
process.wait()
server.terminate()
return success, fail


def write_index_files(output_root, success, fail):
logger.info("generating index files...")
with open(os.path.join(output_root, "index.html"), "w") as outfile:
outfile.write(HTML_HEADER)
for theme, theme_path in sorted(success.items(), key=lambda x: x[0].lower()):
outfile.write(f'<li><a href="{theme}">{theme}<br><img src="_screenshots/{theme}.png"/></a></li>')
outfile.write(HTML_FOOTER.format(success=len(success), fail=len(fail)))

with open(os.path.join(output_root, "failed.html"), "w") as outfile:
outfile.write(HTML_HEADER)
for theme, reason in sorted(fail.items(), key=lambda x: x[0].lower()):
outfile.write(f'<li><h2>{theme}</h2><pre>{reason}</pre></li>')
outfile.write(HTML_FOOTER.format(success=len(success), fail=len(fail)))

logger.info(f"built {len(success)} themes")
logger.info(f"failed {len(fail)} themes")


def parse_args(argv=None):
parser = argparse.ArgumentParser()
parser.add_argument(
"--output", required=False, default="_output",
help="Output folder for generating the theme previews. Defaults to `_output` in themes folder root."
)
parser.add_argument(
"--samples", required=False, default="_pelican/samples",
help="Sample website used to generate theme previews. Defaults to `_pelican/samples` in themes folder root."
)
return parser.parse_args(argv)


def check_requirements():
try:
proc = subprocess.run(
["pelican", "--version"],
check=True, capture_output=True, universal_newlines=True
)
logger.info("using pelican: {}".format(proc.stdout.strip()))
except subprocess.CalledProcessError:
raise RuntimeError("Requires `pelican`, see https://docs.getpelican.com")
try:
proc = subprocess.run(
["shot-scraper", "--version"],
check=True, capture_output=True, universal_newlines=True
)
logger.info("using shot-scraper: {}".format(proc.stdout.strip()))
except subprocess.CalledProcessError:
raise RuntimeError("Requires `shot-scraper`, see https://shot-scraper.data")


def main(argv=None):
check_requirements()
args = parse_args(argv)
theme_root, samples_root, output_root, screenshot_root = setup_folders(args)
success, fail = build_theme_previews(theme_root, samples_root, output_root, screenshot_root)
write_index_files(output_root, success, fail)


if __name__ == "__main__":
main()