Skip to content

Commit

Permalink
Merge pull request #748 from avaris/preview-site
Browse files Browse the repository at this point in the history
  • Loading branch information
justinmayer committed Jul 23, 2023
2 parents da4ceb9 + fa82822 commit d87372a
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 4 deletions.
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/ deploy@pelicanthemes.com:~/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()

0 comments on commit d87372a

Please sign in to comment.