Skip to content

Commit 31f4e67

Browse files
committed
refactor(pages): extracted HTML into site/*.tmpl + scoped workflow triggers
Split the landing page out of build_pages.py into two stand-alone templates (index.html.tmpl + card.html.tmpl) so the HTML can be edited without touching Python. The renderer is now a thin substitute() + read() helper; no Jinja or other runtime deps beyond what was already needed. Trigger filters tightened so cosmetic Pages work never kicks off a 25 min image build or cuts a release: - build.yml paths-ignore: + site/**, + scripts/build_pages.py - release.yml paths-ignore: same - pages.yml: new `push: paths: [site/**, scripts/build_pages.py, .github/workflows/pages.yml]` so edits redeploy the site automatically against the latest release.
1 parent ab42e7a commit 31f4e67

6 files changed

Lines changed: 214 additions & 159 deletions

File tree

.github/workflows/build.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@ on:
1414
- "LICENSE"
1515
- ".gitignore"
1616
- ".github/**"
17+
# Pages site + its renderer live separately from the image build.
18+
- "site/**"
19+
- "scripts/build_pages.py"
1720
pull_request:
1821
branches: [main]
1922
paths-ignore:
2023
- "**.md"
2124
- "docs/**"
2225
- "LICENSE"
2326
- ".github/**"
27+
- "site/**"
28+
- "scripts/build_pages.py"
2429
workflow_dispatch:
2530
inputs:
2631
variant:

.github/workflows/pages.yml

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ name: 🌐 Pages
2222
on:
2323
release:
2424
types: [published, edited, released]
25+
# Re-render the landing page whenever its source changes on main. The
26+
# catalog still points at the LATEST release (resolved at runtime), so
27+
# editing CSS or templates doesn't need a new image build.
28+
push:
29+
branches: [main]
30+
paths:
31+
- 'site/**'
32+
- 'scripts/build_pages.py'
33+
- '.github/workflows/pages.yml'
2534
workflow_dispatch:
2635
inputs:
2736
tag:
@@ -182,14 +191,15 @@ jobs:
182191
run: |
183192
set -euo pipefail
184193
mkdir -p _site
185-
cp site/CNAME _site/CNAME
194+
cp site/CNAME _site/CNAME
186195
cp site/styles.css _site/styles.css
187196
python scripts/build_pages.py \
188-
--manifests release-assets \
189-
--tag "$TAG" \
190-
--repo "$REPO" \
197+
--manifests release-assets \
198+
--templates site \
199+
--tag "$TAG" \
200+
--repo "$REPO" \
191201
--catalog-url "https://$(cat site/CNAME | tr -d '[:space:]')/rpi-imager.json" \
192-
--out _site
202+
--out _site
193203
echo "--- _site tree ---"
194204
ls -la _site
195205

.github/workflows/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ on:
2525
- 'docs/**'
2626
- 'LICENSE'
2727
- '.gitignore'
28+
# Pages site + its renderer are cosmetic; never cut a release for them.
29+
- 'site/**'
30+
- 'scripts/build_pages.py'
2831
workflow_dispatch:
2932
inputs:
3033
force-release:

scripts/build_pages.py

Lines changed: 81 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,24 @@
33
"""
44
BAUER GROUP XPD-RPIImage - Pages site + RPi Imager catalog renderer.
55
6+
Loads HTML templates from disk (site/*.tmpl) and substitutes {{placeholders}}.
7+
No Jinja or other heavy deps - keeps the Pages workflow dependency-free
8+
beyond what the build scripts already need.
9+
610
Input:
7-
--manifests DIR directory with one `*.manifest.json` per variant
8-
(produced by build.yml's checksum step)
9-
--tag vX.Y.Z tag to display on the landing page
10-
--repo owner/name e.g. bauer-group/XPD-RPIImage
11-
--out DIR destination directory for the rendered site
11+
--manifests DIR directory with one *.manifest.json per variant
12+
--tag vX.Y.Z release tag to display
13+
--repo owner/name
14+
--catalog-url URL public URL of the rpi-imager.json
15+
--templates DIR directory containing index.html.tmpl + card.html.tmpl
16+
--out DIR destination directory for the rendered site
1217
1318
Output (in --out):
1419
rpi-imager.json catalog consumed by RPi Imager's Custom Repository
15-
index.html human-facing landing page (rich info + direct flash links)
16-
styles.css minimal, framework-free styling
17-
.nojekyll disables Jekyll on GitHub Pages
20+
index.html human-facing landing page
21+
.nojekyll disables Jekyll on GitHub Pages (we ship pre-rendered HTML)
1822
19-
CNAME is NOT rendered here - the workflow copies it from site/CNAME so the
20-
custom domain lives in git instead of being re-generated on every run.
23+
`styles.css` and `CNAME` are copied by the workflow, not rendered here.
2124
"""
2225
from __future__ import annotations
2326

@@ -30,8 +33,7 @@
3033
from typing import Any
3134

3235

33-
# RPi Imager devices names - mapping our JSON `targets` enum to Imager's
34-
# board identifiers. Entries left out here will be hidden on unknown boards.
36+
# RPi Imager device names - maps our JSON targets to Imager's identifiers.
3537
TARGET_TO_IMAGER_DEVICE = {
3638
"rpi4": "pi4-64bit",
3739
"rpi5": "pi5",
@@ -41,7 +43,6 @@
4143

4244

4345
def load_manifests(directory: Path) -> list[dict[str, Any]]:
44-
"""Read every `*.manifest.json` in `directory`, sorted by variant name."""
4546
paths = sorted(directory.glob("*.manifest.json"))
4647
if not paths:
4748
print(f"error: no *.manifest.json in {directory}", file=sys.stderr)
@@ -53,8 +54,21 @@ def load_manifests(directory: Path) -> list[dict[str, Any]]:
5354
return out
5455

5556

57+
def substitute(template: str, values: dict[str, str]) -> str:
58+
"""Replace every `{{key}}` with `values[key]`.
59+
60+
Values that are missing are kept as-is so typos are visible in the
61+
rendered output rather than silently expanding to empty. No nested
62+
substitution, no conditionals - if the template grows logic beyond
63+
this, promote to Jinja2 with a proper test suite.
64+
"""
65+
out = template
66+
for key, value in values.items():
67+
out = out.replace("{{" + key + "}}", value)
68+
return out
69+
70+
5671
def render_rpi_imager_json(manifests: list[dict[str, Any]], tag: str) -> dict[str, Any]:
57-
"""Produce the RPi Imager Custom Repository JSON."""
5872
os_list: list[dict[str, Any]] = []
5973
for m in manifests:
6074
variant = m["variant"]
@@ -80,163 +94,76 @@ def render_rpi_imager_json(manifests: list[dict[str, Any]], tag: str) -> dict[st
8094
}
8195

8296

83-
def render_index_html(manifests: list[dict[str, Any]], tag: str, repo: str, catalog_url: str) -> str:
84-
"""Produce the standalone landing page."""
97+
def render_card(template: str, manifest: dict[str, Any], tag: str) -> str:
98+
img = manifest["image"]
99+
sha256_url = img["url"] + ".sha256"
100+
# bgrpiimage-<variant>-vX.Y.Z.img.xz → bgrpiimage-<variant>-vX.Y.Z.manifest.json
101+
manifest_url = img["url"].rsplit(".img.xz", 1)[0] + ".manifest.json"
102+
values = {
103+
"variant": html.escape(manifest["variant"]),
104+
"tag": html.escape(tag),
105+
"description": html.escape(manifest.get("description") or ""),
106+
"hostname": html.escape(manifest.get("hostname") or "-"),
107+
"targets": html.escape(", ".join(manifest["targets"])),
108+
"download_mib": f"{img['download_size'] / 1024 / 1024:,.1f}",
109+
"extract_mib": f"{img['extract_size'] / 1024 / 1024:,.1f}",
110+
"release_date": html.escape(manifest.get("release_date") or "-"),
111+
"download_sha256": html.escape(img["download_sha256"]),
112+
"extract_sha256": html.escape(img["extract_sha256"]),
113+
"image_url": html.escape(img["url"]),
114+
"sha256_url": html.escape(sha256_url),
115+
"manifest_url": html.escape(manifest_url),
116+
}
117+
return substitute(template, values)
118+
119+
120+
def render_index_html(
121+
index_template: str,
122+
card_template: str,
123+
manifests: list[dict[str, Any]],
124+
tag: str,
125+
repo: str,
126+
catalog_url: str,
127+
) -> str:
128+
cards = "\n".join(render_card(card_template, m, tag) for m in manifests)
85129
generated = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
86-
cards = []
87-
for m in manifests:
88-
variant = m["variant"]
89-
img = m["image"]
90-
targets = ", ".join(m["targets"])
91-
download_mb = img["download_size"] / 1024 / 1024
92-
extract_mb = img["extract_size"] / 1024 / 1024
93-
card = f"""
94-
<article class="card">
95-
<header>
96-
<h2>{html.escape(variant)}</h2>
97-
<span class="tag">{html.escape(tag)}</span>
98-
</header>
99-
<p class="description">{html.escape(m.get('description') or '')}</p>
100-
<dl class="meta">
101-
<dt>Hostname</dt><dd><code>{html.escape(m.get('hostname') or '-')}</code></dd>
102-
<dt>Targets</dt><dd>{html.escape(targets)}</dd>
103-
<dt>Download size</dt><dd>{download_mb:,.1f} MiB</dd>
104-
<dt>Extracted size</dt><dd>{extract_mb:,.1f} MiB</dd>
105-
<dt>Release date</dt><dd>{html.escape(m.get('release_date') or '-')}</dd>
106-
</dl>
107-
<div class="checksums">
108-
<div><span class="label">SHA-256 (.img.xz)</span><code>{html.escape(img['download_sha256'])}</code></div>
109-
<div><span class="label">SHA-256 (.img)</span><code>{html.escape(img['extract_sha256'])}</code></div>
110-
</div>
111-
<div class="actions">
112-
<a class="btn primary" href="{html.escape(img['url'])}">⬇ Download .img.xz</a>
113-
<a class="btn" href="{html.escape(img['url'])}.sha256">.sha256</a>
114-
<a class="btn" href="{html.escape(img['url']).rsplit('.img.xz', 1)[0]}.manifest.json">manifest.json</a>
115-
</div>
116-
</article>
117-
"""
118-
cards.append(card)
119-
120-
cards_html = "\n".join(cards)
121-
catalog_copy_id = "catalog-url"
122-
safe_repo = html.escape(repo)
123-
safe_tag = html.escape(tag)
124-
safe_catalog = html.escape(catalog_url)
125-
safe_generated = html.escape(generated)
126-
127-
return f"""<!DOCTYPE html>
128-
<html lang="en">
129-
<head>
130-
<meta charset="utf-8" />
131-
<meta name="viewport" content="width=device-width, initial-scale=1" />
132-
<title>BAUER GROUP RPIImage – latest release {safe_tag}</title>
133-
<meta name="description" content="Declarative, reproducible Raspberry Pi OS images by BAUER GROUP. RPi Imager catalog + direct downloads." />
134-
<link rel="stylesheet" href="styles.css" />
135-
<link rel="icon" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20100%20100%22%3E%3Crect%20width%3D%22100%22%20height%3D%22100%22%20rx%3D%2220%22%20fill%3D%22%23e80d2e%22%2F%3E%3Ctext%20x%3D%2250%22%20y%3D%2268%22%20font-family%3D%22sans-serif%22%20font-size%3D%2258%22%20font-weight%3D%22900%22%20fill%3D%22white%22%20text-anchor%3D%22middle%22%3EB%3C%2Ftext%3E%3C%2Fsvg%3E" />
136-
</head>
137-
<body>
138-
<header class="hero">
139-
<div class="inner">
140-
<h1>📦 BAUER GROUP <span class="accent">RPIImage</span></h1>
141-
<p class="subtitle">Declarative, reproducible, CI-built Raspberry Pi OS images for production and development.</p>
142-
<p class="release">Current release: <a href="https://github.com/{safe_repo}/releases/tag/{safe_tag}"><strong>{safe_tag}</strong></a></p>
143-
</div>
144-
</header>
145-
146-
<main>
147-
<section class="pitch">
148-
<h2>🛠 Add this repo to Raspberry Pi Imager</h2>
149-
<ol class="steps">
150-
<li>Install <a href="https://www.raspberrypi.com/software/">Raspberry Pi Imager</a> (v1.8.5 or later).</li>
151-
<li>Open it and go to <strong>Settings</strong> (the ⚙ icon) → <strong>Custom repository</strong>.</li>
152-
<li>Paste the catalog URL below and close the settings dialog:
153-
<div class="copyrow">
154-
<code id="{catalog_copy_id}">{safe_catalog}</code>
155-
<button type="button" onclick="navigator.clipboard.writeText(document.getElementById('{catalog_copy_id}').innerText)">Copy</button>
156-
</div>
157-
</li>
158-
<li>Restart Imager. Our variants now appear under <strong>Operating System → BAUER GROUP</strong>.</li>
159-
<li>Pick your target device → pick an image → flash.</li>
160-
</ol>
161-
162-
<h3>🧷 Flashing a Compute Module (CM4 / CM5 eMMC)</h3>
163-
<ol>
164-
<li>Put the carrier board in <strong>rpiboot mode</strong>:
165-
<ul>
166-
<li><strong>CM4 IO-Board</strong>: fit the jumper on <code>J2</code> (Disable eMMC Boot).</li>
167-
<li><strong>CM5 IO-Board</strong>: bridge the <code>nRPIBOOT</code> test pad (or set the fit-jumper where provided).</li>
168-
</ul>
169-
</li>
170-
<li>Connect the <strong>USB-C slave port</strong> (not the host USB) to your machine.</li>
171-
<li>Power the board. RPi Imager (≥ 1.8.5) detects the CM as <em>"RPi"</em> mass storage via its built-in <code>rpiboot</code>.</li>
172-
<li>Flash as usual.</li>
173-
<li>Remove the jumper, re-power, boot.</li>
174-
</ol>
175-
</section>
176-
177-
<section class="variants">
178-
<h2>🧩 Variants in this release</h2>
179-
<div class="cards">
180-
{cards_html}
181-
</div>
182-
</section>
183-
184-
<section class="security">
185-
<h2>🔐 Default credentials</h2>
186-
<p>These images ship with <strong>demo credentials</strong> - safe only on an isolated lab network:</p>
187-
<ul>
188-
<li><code>admin</code> password → <code>12345678</code></li>
189-
<li>WiFi PSK for <code>IOT @ BAUER-GROUP</code> → <code>12345678</code></li>
190-
</ul>
191-
<p>Bake real values at build time via <code>.env</code>, or change them post-flash with:</p>
192-
<pre><code>sudo bgrpiimage-setup password
193-
sudo bgrpiimage-setup wifi "MyNet" "s3cret" DE
194-
sudo bgrpiimage-setup ip eth0 static 10.0.0.5/24 10.0.0.1 1.1.1.1</code></pre>
195-
</section>
196-
197-
<section class="meta-links">
198-
<h2>📚 More</h2>
199-
<ul>
200-
<li><a href="https://github.com/{safe_repo}">Repository on GitHub</a></li>
201-
<li><a href="https://github.com/{safe_repo}/releases">All releases</a></li>
202-
<li><a href="https://github.com/{safe_repo}/blob/main/docs/hardware.md">Hardware reference</a></li>
203-
<li><a href="https://github.com/{safe_repo}/blob/main/docs/flash.md">Flashing guide</a></li>
204-
<li><a href="https://github.com/{safe_repo}/blob/main/docs/configuration.md">JSON configuration reference</a></li>
205-
</ul>
206-
</section>
207-
</main>
208-
209-
<footer>
210-
<p>© BAUER GROUP · <a href="https://github.com/{safe_repo}/blob/main/LICENSE">MIT</a> · generated {safe_generated}</p>
211-
</footer>
212-
</body>
213-
</html>
214-
"""
130+
return substitute(index_template, {
131+
"tag": html.escape(tag),
132+
"repo": html.escape(repo),
133+
"catalog_url": html.escape(catalog_url),
134+
"cards": cards,
135+
"generated": html.escape(generated),
136+
})
215137

216138

217139
def main() -> int:
218140
p = argparse.ArgumentParser(description=__doc__)
219-
p.add_argument("--manifests", required=True, type=Path)
220-
p.add_argument("--tag", required=True)
221-
p.add_argument("--repo", required=True)
222-
p.add_argument("--catalog-url", required=True,
223-
help="Public URL where rpi-imager.json will be served.")
224-
p.add_argument("--out", required=True, type=Path)
141+
p.add_argument("--manifests", required=True, type=Path)
142+
p.add_argument("--tag", required=True)
143+
p.add_argument("--repo", required=True)
144+
p.add_argument("--catalog-url", required=True)
145+
p.add_argument("--templates", required=True, type=Path,
146+
help="directory with index.html.tmpl + card.html.tmpl")
147+
p.add_argument("--out", required=True, type=Path)
225148
args = p.parse_args()
226149

227150
args.out.mkdir(parents=True, exist_ok=True)
228151
manifests = load_manifests(args.manifests)
229152

153+
# --- rpi-imager.json -----------------------------------------------------
230154
catalog = render_rpi_imager_json(manifests, args.tag)
231155
(args.out / "rpi-imager.json").write_text(
232156
json.dumps(catalog, indent=2) + "\n", encoding="utf-8"
233157
)
234158

235-
html_text = render_index_html(manifests, args.tag, args.repo, args.catalog_url)
159+
# --- index.html ----------------------------------------------------------
160+
index_tmpl = (args.templates / "index.html.tmpl").read_text(encoding="utf-8")
161+
card_tmpl = (args.templates / "card.html.tmpl").read_text(encoding="utf-8")
162+
html_text = render_index_html(index_tmpl, card_tmpl, manifests,
163+
args.tag, args.repo, args.catalog_url)
236164
(args.out / "index.html").write_text(html_text, encoding="utf-8")
237165

238-
# .nojekyll disables the default Jekyll build on GitHub Pages - we ship
239-
# pre-rendered HTML and don't want Jekyll to mangle it.
166+
# --- .nojekyll -----------------------------------------------------------
240167
(args.out / ".nojekyll").write_text("", encoding="utf-8")
241168

242169
print(f"wrote {len(manifests)} variant(s) into {args.out}", file=sys.stderr)

site/card.html.tmpl

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<article class="card">
2+
<header>
3+
<h2>{{variant}}</h2>
4+
<span class="tag">{{tag}}</span>
5+
</header>
6+
<p class="description">{{description}}</p>
7+
<dl class="meta">
8+
<dt>Hostname</dt><dd><code>{{hostname}}</code></dd>
9+
<dt>Targets</dt><dd>{{targets}}</dd>
10+
<dt>Download size</dt><dd>{{download_mib}} MiB</dd>
11+
<dt>Extracted size</dt><dd>{{extract_mib}} MiB</dd>
12+
<dt>Release date</dt><dd>{{release_date}}</dd>
13+
</dl>
14+
<div class="checksums">
15+
<div><span class="label">SHA-256 (.img.xz)</span><code>{{download_sha256}}</code></div>
16+
<div><span class="label">SHA-256 (.img)</span><code>{{extract_sha256}}</code></div>
17+
</div>
18+
<div class="actions">
19+
<a class="btn primary" href="{{image_url}}">⬇ Download .img.xz</a>
20+
<a class="btn" href="{{sha256_url}}">.sha256</a>
21+
<a class="btn" href="{{manifest_url}}">manifest.json</a>
22+
</div>
23+
</article>

0 commit comments

Comments
 (0)