slides: frame a 'How to implement a TAPPaaS module' deck
All checks were successful
Build docs site / build (push) Successful in 49s
Build slides / build (push) Successful in 1m16s

Adds a second Marp source root. Decks under slides/ build to an output path
mirroring their repo path, so slides/TAPPaaS/HOW-TO/NewModule/index.md is
served at slides.makerfloss.eu/TAPPaaS/HOW-TO/NewModule/. Decks in
docs/presentations/ keep their flat URLs.

The deck itself is a frame: structure and headings from the real
src/apps/00-Template anatomy, content still to be written. Unfinished spots
are written as inline `TODO: ...` and rendered in red so a half-finished
deck cannot be presented by accident.

Also fixes the Docker fallback, which could not write to a mktemp directory
on macOS (not shared with Docker Desktop) and must not be passed --user.
This commit is contained in:
Lars Rossen 2026-08-22 15:57:58 +02:00
parent 4abc634443
commit 91f53f1f62
5 changed files with 342 additions and 53 deletions

5
.gitignore vendored
View file

@ -2,8 +2,9 @@
*.retry
# ---> Generated slides (built by marp on server or locally via build-slides.sh)
slides/*.html
slides/*.pdf
# Deck *sources* also live under slides/, so ignore only the build output.
slides/**/*.html
slides/**/*.pdf
# ---> MkDocs build output
site/

View file

@ -27,13 +27,15 @@ docs/ # everything here is built and shipped to docs.makerfloss.eu
hardware/ # auto-indexed per-host frontmatter (srv01..srv05, makerfloss.eu)
services/ # auto-indexed per-service frontmatter (docs, forgejo, …)
infrastructure/ # labdesign, VPS/DNS, etc.
presentations/ # Marp decks (build-slides.sh)
presentations/ # Marp decks, flat URLs (build-slides.sh)
notes/ # repo-only working material, not built
meetings/ # meeting notes (Danish allowed)
todo/ # task lists, working norms, wishlist, services
dev/ # internal plans/ and specs/
communications/ # community comms artifacts (Facebook posts, etc.)
sandbox/ # scratch / pipeline fixtures (e.g. test-mermaid.md)
slides/ # build output — AND Marp deck sources whose URL is a path,
# e.g. slides/tappaas/how-to/new-module/index.md
```
## Infrastructure

View file

@ -1,4 +1,14 @@
#!/usr/bin/env bash
# Build every Marp deck in the repository into static HTML.
#
# A deck is any *.md carrying `marp: true` in its front matter. Two source
# roots, two output conventions:
#
# docs/presentations/<name>.md -> <name>.html (flat, historical)
# slides/<path>/<name>.md -> <path>/<name>.html (path mirrors URL)
#
# So slides/tappaas/how-to/new-module/index.md is served at
# https://slides.makerfloss.eu/tappaas/how-to/new-module/ .
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@ -8,53 +18,74 @@ REPO_WEB_URL="${REPO_WEB_URL:-https://forgejo.makerfloss.eu/sjat/MakerFLOSS}"
mkdir -p "$OUTPUT_DIR"
# Wipe previously built slides so deleted/de-tagged .md files disappear.
find "$OUTPUT_DIR" -maxdepth 1 -name "*.html" -delete
# Find all markdown files with marp: true frontmatter
SLIDES=()
# Collect decks as "<repo-relative source>|<output-relative target>" pairs.
DECKS=()
while IFS= read -r f; do
SLIDES+=("$f")
done < <(grep -rl "^marp: true" "$REPO_ROOT/docs" --include="*.md" 2>/dev/null || true)
rel="${f#"$REPO_ROOT"/}"
DECKS+=("$rel|$(basename "${rel%.md}").html")
done < <(grep -rl "^marp: true" "$REPO_ROOT/docs" --include="*.md" 2>/dev/null | sort || true)
if [ ${#SLIDES[@]} -eq 0 ]; then
echo "No marp presentations found in docs/."
if [ -d "$REPO_ROOT/slides" ]; then
while IFS= read -r f; do
rel="${f#"$REPO_ROOT"/}"
out="${rel#slides/}"
DECKS+=("$rel|${out%.md}.html")
done < <(grep -rl "^marp: true" "$REPO_ROOT/slides" --include="*.md" 2>/dev/null | sort || true)
fi
if [ ${#DECKS[@]} -eq 0 ]; then
echo "No marp presentations found."
exit 0
fi
echo "Found ${#SLIDES[@]} presentation(s):"
printf ' %s\n' "${SLIDES[@]}"
echo "Found ${#DECKS[@]} presentation(s):"
printf ' %s\n' "${DECKS[@]%%|*}"
# Create temp output directory for Docker fallback
TEMP_OUTPUT=$(mktemp -d)
chmod 777 "$TEMP_OUTPUT"
trap "rm -rf '$TEMP_OUTPUT'" EXIT
# Wipe previously built HTML so deleted/de-tagged decks disappear. Only *.html
# is removed, so deck sources living inside slides/ are never touched.
find "$OUTPUT_DIR" -name "*.html" -type f -delete
render() { # render <source-rel> <target-rel>
local src="$1" dst="$2"
mkdir -p "$OUTPUT_DIR/$(dirname "$dst")"
if command -v marp &>/dev/null; then
for slide in "${SLIDES[@]}"; do
BASENAME=$(basename "${slide%.*}")
marp --html --output "$OUTPUT_DIR/${BASENAME}.html" "$slide"
done
marp --html --output "$OUTPUT_DIR/$dst" "$REPO_ROOT/$src"
elif command -v npx &>/dev/null; then
npx --yes @marp-team/marp-cli --html --output "$OUTPUT_DIR/$dst" "$REPO_ROOT/$src"
else
echo "marp not found locally — using Docker (marpteam/marp-cli)..."
for slide in "${SLIDES[@]}"; do
REL_SLIDE="${slide#${REPO_ROOT}/}"
BASENAME=$(basename "${slide%.*}")
# marpteam/marp-cli drops privileges to its own "marp" user, so the mounted
# output tree has to be writable by anyone — and --user must NOT be passed.
chmod -R a+rwX "$OUTPUT_DIR"
docker run --rm \
-v "$REPO_ROOT":/home/marp/app:ro \
-v "$TEMP_OUTPUT":/home/marp/output \
marpteam/marp-cli --html --output "/home/marp/output/${BASENAME}.html" "$REL_SLIDE"
done
cp "$TEMP_OUTPUT"/*.html "$OUTPUT_DIR/" 2>/dev/null || true
-v "$OUTPUT_DIR":/home/marp/out \
marpteam/marp-cli --html --output "/home/marp/out/$dst" "$src"
fi
}
if ! command -v marp &>/dev/null && ! command -v npx &>/dev/null; then
if command -v docker &>/dev/null; then
echo "marp not found locally — using Docker (marpteam/marp-cli)..."
else
echo "error: need one of marp, npx or docker on PATH" >&2
exit 1
fi
fi
# Inject mermaid.js into any HTML that contains mermaid code blocks.
for deck in "${DECKS[@]}"; do
render "${deck%%|*}" "${deck#*|}"
done
# Post-process each built page: highlight unfinished `TODO ...` markers, and
# turn mermaid code blocks into live diagrams.
#
# Marp emits fenced mermaid blocks as <pre><code class="language-mermaid">.
# The script finds those elements, replaces them with <div class="mermaid">,
# then loads and runs mermaid.js from CDN.
inject_mermaid() {
# The script replaces those elements with <div class="mermaid">, then loads and
# runs mermaid.js from CDN.
postprocess() {
local html_file="$1"
python3 - "$html_file" << 'PYEOF'
import re
import sys
path = sys.argv[1]
@ -73,25 +104,33 @@ await mermaid.run();
with open(path, encoding='utf-8') as f:
content = f.read()
new_content = content.replace('</body>', snippet + '\n</body>', 1)
if new_content == content:
# Inline <code>TODO…</code> -> <code class="todo">…</code>; decks style that class.
content, todos = re.subn(r'<code>(TODO\b[^<]*)</code>', r'<code class="todo">\1</code>', content)
if 'class="language-mermaid"' in content:
if '</body>' not in content:
print(f"Warning: </body> not found in {path}", file=sys.stderr)
sys.exit(1)
content = content.replace('</body>', snippet + '\n</body>', 1)
print(f" Injected mermaid.js into {path}")
with open(path, 'w', encoding='utf-8') as f:
f.write(new_content)
f.write(content)
if todos:
print(f" {todos} unfinished TODO marker(s) in {path}")
PYEOF
}
if command -v python3 &>/dev/null; then
for html_file in "$OUTPUT_DIR"/*.html; do
[ -f "$html_file" ] || continue
if grep -q 'class="language-mermaid"' "$html_file"; then
inject_mermaid "$html_file"
echo " Injected mermaid.js into $(basename "$html_file")"
fi
done
# The landing index.html is generated further down, so every *.html present
# right now is a built deck.
while IFS= read -r html_file; do
postprocess "$html_file"
done < <(find "$OUTPUT_DIR" -name "*.html" -type f)
else
echo "Warning: python3 not found — skipping mermaid injection"
echo "Warning: python3 not found — skipping post-processing"
fi
# Regenerate index.html listing every built deck.
@ -113,9 +152,10 @@ INDEX="$OUTPUT_DIR/index.html"
printf '<p class="sub">Built from <a href="%s">%s</a></p>' \
"$REPO_WEB_URL" "${REPO_WEB_URL#https://}"
printf '<ul>'
find "$OUTPUT_DIR" -maxdepth 1 -name "*.html" ! -name "index.html" | sort | while IFS= read -r html; do
name=$(basename "$html" .html)
printf '<li><a href="%s">%s</a></li>' "$(basename "$html")" "$name"
for deck in "${DECKS[@]}"; do
href="${deck#*|}"
label="${href%/index.html}"; label="${label%.html}"
printf '<li><a href="%s">%s</a></li>' "$href" "$label"
done
printf '</ul>'
printf '<footer>Last built: %s &middot; <a href="https://marp.app">Marp</a></footer>' "$(date -Iseconds)"

View file

@ -11,4 +11,18 @@ tls: letsencrypt
## Notes
Slide-deck site. Decks are authored as Marp markdown in `docs/presentations/` and compiled to HTML by `build-slides.sh` (CI invokes it via the [marp](marp.md) toolchain). Built output is rsynced to the VPS and served alongside [docs](docs.md).
Slide-deck site. Decks are authored as Marp markdown and compiled to HTML by
`build-slides.sh` (CI invokes it via the [marp](marp.md) toolchain). Built output is rsynced to
the VPS and served alongside [docs](docs.md).
Two source roots, two output conventions:
| Source | Published at |
| --- | --- |
| `docs/presentations/<name>.md` | `https://slides.makerfloss.eu/<name>.html` |
| `slides/<path>/<name>.md` | `https://slides.makerfloss.eu/<path>/<name>.html` |
Use `slides/` when a deck belongs to a named series and the URL should read like a path — for
example `slides/tappaas/how-to/new-module/index.md` is served at
`https://slides.makerfloss.eu/tappaas/how-to/new-module/`. Only the markdown is committed; the
generated HTML under `slides/` is git-ignored.

View file

@ -0,0 +1,232 @@
---
marp: true
theme: gaia
class: invert
paginate: true
title: How to implement a TAPPaaS module
description: Walk-through of adding a new module to the TAPPaaS repository
---
<style>
section { font-size: 26px; }
h1 { font-size: 1.5em; }
h2 { font-size: 1.15em; }
pre { font-size: 0.72em; line-height: 1.35; }
table { font-size: 0.8em; }
th, td { padding: 0.25em 0.6em; }
/* Unfinished content, loud on purpose: an unfinished deck should never be
presented by accident. Written in the markdown as `TODO: ...`. */
code.todo { color: #ff8a80; font-weight: 600; }
div.mermaid { display: flex; justify-content: center; }
</style>
<!-- _paginate: false -->
# How to implement a TAPPaaS module
From empty directory to merged pull request
<!--
Speaker notes go in HTML comments; press `p` in the browser for presenter view.
TODO: 30-second framing — who is in the room, what they walk out able to do.
-->
---
## Who this is for
- You want to add an **app** or a **foundation** capability to TAPPaaS
- You can read shell, JSON, and a little Nix
- You have a TAPPaaS test environment you are allowed to break
**Prerequisites**
- A clone of `TAPPaaS/TAPPaaS`, branch `TODO: ADR007 or stable`
- SSH access to a test site
- `TODO: tooling list`
---
## Vocabulary in 60 seconds
| Term | Means |
| --- | --- |
| **Module** | One installable unit under `src/apps/` or `src/foundation/` |
| **Service** | A part of a module with its own lifecycle scripts |
| **Manager** | Owns desired state for a domain |
| **Controller** | Applies desired state to one concrete technology |
`TODO: tighten against ADR-007 taxonomy; drop the terms that do not matter here`
---
## Anatomy of a module
```text
src/apps/<module>/
├── README.md # what it is, why it exists
├── DESIGN.md # how it is put together
├── INSTALL.md # operator-facing install notes
├── UPGRADE.md # operator-facing upgrade notes
├── AUTHORS.md # who to ask
├── LICENSE
├── <module>.json # declaration: the module's contract
├── <module>.nix # runtime definition
├── install.sh # create it
├── update.sh # move it forward
├── test.sh # prove it works
└── services/<svc>/ # per-service install/update/test/delete
```
---
## The path from nothing to merged
```mermaid
flowchart LR
A[Scaffold from<br/>00-Template] --> B[Declare<br/>module.json]
B --> C[Define runtime<br/>module.nix]
C --> D[install.sh]
D --> E[services/]
E --> F[update.sh]
F --> G[test.sh]
G --> H[Docs]
H --> I[PR + CI]
```
---
## Step 1 — Scaffold
```bash
cp -r src/apps/00-Template src/apps/<module>
```
- Rename `template.json` → `<module>.json`, `template.nix` → `<module>.nix`
- Read `README-template.md`, then delete it
- `TODO: is there a scaffold script? if not, should there be?`
---
## Step 2 — Declare the module
`<module>.json`
```json
{
"TODO": "smallest working declaration"
}
```
- What the module *is*, not how it is built
- `TODO: required keys, optional keys, validation`
---
## Step 3 — Define the runtime
`<module>.nix`
```nix
# TODO: minimal example
```
- `TODO: what belongs in nix vs. what belongs in install.sh`
---
## Step 4 — `install.sh`
- Idempotent: safe to run twice
- Fails loudly, never half-way
- `pre-install.sh` when something must exist first
```bash
# TODO: skeleton with the house conventions
```
---
## Step 5 — Services
```text
services/<svc>/install-service.sh
services/<svc>/update-service.sh
services/<svc>/test-service.sh
services/<svc>/delete-service.sh
```
- One service = one thing with its own lifecycle
- `TODO: when to split into services vs. keep it in the module`
---
## Step 6 — `update.sh`
- Upgrade in place, no data loss
- Version pinning: `TODO`
- Rollback story: `TODO`
---
## Step 7 — `test.sh`
- Runs against a real environment, not a mock
- Exit non-zero on failure, quiet on success
- `TODO: what CI runs, and what only a human can check`
---
## Step 8 — Documentation
| File | Answers |
| --- | --- |
| `README.md` | What is this and why would I install it? |
| `DESIGN.md` | How is it built, what were the trade-offs? |
| `INSTALL.md` | How do I install it, step by step? |
| `UPGRADE.md` | What breaks between versions? |
Docs are synced to the docs site — a missing README is a missing page.
---
## Try it on a test site
```bash
# TODO: the real loop
```
1. Deploy to the test environment
2. Run `test.sh`
3. Break it on purpose, run it again
---
## Submit
- Branch: `TODO: naming convention`
- One module per pull request
- CI must be green
- Review checklist:
- [ ] Idempotent install
- [ ] `test.sh` passes on a clean site
- [ ] Docs complete
- [ ] `TODO`
---
## Pitfalls
- `TODO: the three mistakes reviewers actually see`
- `TODO`
- `TODO`
---
## Where to go next
- `src/apps/00-Template/` — the canonical starting point
- `docs/ADR/ADR-007 - TAPPaaS Taxonomy.md`
- `TODO: contribution guide link`
**Questions?**