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 *.retry
# ---> Generated slides (built by marp on server or locally via build-slides.sh) # ---> Generated slides (built by marp on server or locally via build-slides.sh)
slides/*.html # Deck *sources* also live under slides/, so ignore only the build output.
slides/*.pdf slides/**/*.html
slides/**/*.pdf
# ---> MkDocs build output # ---> MkDocs build output
site/ 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) hardware/ # auto-indexed per-host frontmatter (srv01..srv05, makerfloss.eu)
services/ # auto-indexed per-service frontmatter (docs, forgejo, …) services/ # auto-indexed per-service frontmatter (docs, forgejo, …)
infrastructure/ # labdesign, VPS/DNS, etc. 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 notes/ # repo-only working material, not built
meetings/ # meeting notes (Danish allowed) meetings/ # meeting notes (Danish allowed)
todo/ # task lists, working norms, wishlist, services todo/ # task lists, working norms, wishlist, services
dev/ # internal plans/ and specs/ dev/ # internal plans/ and specs/
communications/ # community comms artifacts (Facebook posts, etc.) communications/ # community comms artifacts (Facebook posts, etc.)
sandbox/ # scratch / pipeline fixtures (e.g. test-mermaid.md) 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 ## Infrastructure

View file

@ -1,4 +1,14 @@
#!/usr/bin/env bash #!/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 set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 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" mkdir -p "$OUTPUT_DIR"
# Wipe previously built slides so deleted/de-tagged .md files disappear. # Collect decks as "<repo-relative source>|<output-relative target>" pairs.
find "$OUTPUT_DIR" -maxdepth 1 -name "*.html" -delete DECKS=()
# Find all markdown files with marp: true frontmatter
SLIDES=()
while IFS= read -r f; do while IFS= read -r f; do
SLIDES+=("$f") rel="${f#"$REPO_ROOT"/}"
done < <(grep -rl "^marp: true" "$REPO_ROOT/docs" --include="*.md" 2>/dev/null || true) 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 if [ -d "$REPO_ROOT/slides" ]; then
echo "No marp presentations found in docs/." 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 exit 0
fi fi
echo "Found ${#SLIDES[@]} presentation(s):" echo "Found ${#DECKS[@]} presentation(s):"
printf ' %s\n' "${SLIDES[@]}" printf ' %s\n' "${DECKS[@]%%|*}"
# Create temp output directory for Docker fallback # Wipe previously built HTML so deleted/de-tagged decks disappear. Only *.html
TEMP_OUTPUT=$(mktemp -d) # is removed, so deck sources living inside slides/ are never touched.
chmod 777 "$TEMP_OUTPUT" find "$OUTPUT_DIR" -name "*.html" -type f -delete
trap "rm -rf '$TEMP_OUTPUT'" EXIT
if command -v marp &>/dev/null; then render() { # render <source-rel> <target-rel>
for slide in "${SLIDES[@]}"; do local src="$1" dst="$2"
BASENAME=$(basename "${slide%.*}") mkdir -p "$OUTPUT_DIR/$(dirname "$dst")"
marp --html --output "$OUTPUT_DIR/${BASENAME}.html" "$slide" if command -v marp &>/dev/null; then
done marp --html --output "$OUTPUT_DIR/$dst" "$REPO_ROOT/$src"
else elif command -v npx &>/dev/null; then
echo "marp not found locally — using Docker (marpteam/marp-cli)..." npx --yes @marp-team/marp-cli --html --output "$OUTPUT_DIR/$dst" "$REPO_ROOT/$src"
for slide in "${SLIDES[@]}"; do else
REL_SLIDE="${slide#${REPO_ROOT}/}" # marpteam/marp-cli drops privileges to its own "marp" user, so the mounted
BASENAME=$(basename "${slide%.*}") # output tree has to be writable by anyone — and --user must NOT be passed.
chmod -R a+rwX "$OUTPUT_DIR"
docker run --rm \ docker run --rm \
-v "$REPO_ROOT":/home/marp/app:ro \ -v "$REPO_ROOT":/home/marp/app:ro \
-v "$TEMP_OUTPUT":/home/marp/output \ -v "$OUTPUT_DIR":/home/marp/out \
marpteam/marp-cli --html --output "/home/marp/output/${BASENAME}.html" "$REL_SLIDE" marpteam/marp-cli --html --output "/home/marp/out/$dst" "$src"
done fi
cp "$TEMP_OUTPUT"/*.html "$OUTPUT_DIR/" 2>/dev/null || true }
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 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">. # Marp emits fenced mermaid blocks as <pre><code class="language-mermaid">.
# The script finds those elements, replaces them with <div class="mermaid">, # The script replaces those elements with <div class="mermaid">, then loads and
# then loads and runs mermaid.js from CDN. # runs mermaid.js from CDN.
inject_mermaid() { postprocess() {
local html_file="$1" local html_file="$1"
python3 - "$html_file" << 'PYEOF' python3 - "$html_file" << 'PYEOF'
import re
import sys import sys
path = sys.argv[1] path = sys.argv[1]
@ -73,25 +104,33 @@ await mermaid.run();
with open(path, encoding='utf-8') as f: with open(path, encoding='utf-8') as f:
content = f.read() 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) print(f"Warning: </body> not found in {path}", file=sys.stderr)
sys.exit(1) 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: 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 PYEOF
} }
if command -v python3 &>/dev/null; then if command -v python3 &>/dev/null; then
for html_file in "$OUTPUT_DIR"/*.html; do # The landing index.html is generated further down, so every *.html present
[ -f "$html_file" ] || continue # right now is a built deck.
if grep -q 'class="language-mermaid"' "$html_file"; then while IFS= read -r html_file; do
inject_mermaid "$html_file" postprocess "$html_file"
echo " Injected mermaid.js into $(basename "$html_file")" done < <(find "$OUTPUT_DIR" -name "*.html" -type f)
fi
done
else else
echo "Warning: python3 not found — skipping mermaid injection" echo "Warning: python3 not found — skipping post-processing"
fi fi
# Regenerate index.html listing every built deck. # 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>' \ printf '<p class="sub">Built from <a href="%s">%s</a></p>' \
"$REPO_WEB_URL" "${REPO_WEB_URL#https://}" "$REPO_WEB_URL" "${REPO_WEB_URL#https://}"
printf '<ul>' printf '<ul>'
find "$OUTPUT_DIR" -maxdepth 1 -name "*.html" ! -name "index.html" | sort | while IFS= read -r html; do for deck in "${DECKS[@]}"; do
name=$(basename "$html" .html) href="${deck#*|}"
printf '<li><a href="%s">%s</a></li>' "$(basename "$html")" "$name" label="${href%/index.html}"; label="${label%.html}"
printf '<li><a href="%s">%s</a></li>' "$href" "$label"
done done
printf '</ul>' printf '</ul>'
printf '<footer>Last built: %s &middot; <a href="https://marp.app">Marp</a></footer>' "$(date -Iseconds)" 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 ## 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?**