diff --git a/.gitignore b/.gitignore index 8021355..b21e050 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/CLAUDE.md b/CLAUDE.md index 80bd579..6f6addd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/build-slides.sh b/build-slides.sh index e5c74da..34d3d6a 100755 --- a/build-slides.sh +++ b/build-slides.sh @@ -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/.md -> .html (flat, historical) +# slides//.md -> /.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 "|" 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 -if command -v marp &>/dev/null; then - for slide in "${SLIDES[@]}"; do - BASENAME=$(basename "${slide%.*}") - marp --html --output "$OUTPUT_DIR/${BASENAME}.html" "$slide" - done -else - echo "marp not found locally — using Docker (marpteam/marp-cli)..." - for slide in "${SLIDES[@]}"; do - REL_SLIDE="${slide#${REPO_ROOT}/}" - BASENAME=$(basename "${slide%.*}") +render() { # render + local src="$1" dst="$2" + mkdir -p "$OUTPUT_DIR/$(dirname "$dst")" + if command -v marp &>/dev/null; then + 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 + # 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
.
-# The script finds those elements, replaces them with 
, -# then loads and runs mermaid.js from CDN. -inject_mermaid() { +# The script replaces those elements with
, 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('', snippet + '\n', 1) -if new_content == content: - print(f"Warning: not found in {path}", file=sys.stderr) - sys.exit(1) + +# Inline TODO… -> …; decks style that class. +content, todos = re.subn(r'(TODO\b[^<]*)', r'\1', content) + +if 'class="language-mermaid"' in content: + if '' not in content: + print(f"Warning: not found in {path}", file=sys.stderr) + sys.exit(1) + content = content.replace('', snippet + '\n', 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 '

Built from %s

' \ "$REPO_WEB_URL" "${REPO_WEB_URL#https://}" printf '
    ' - find "$OUTPUT_DIR" -maxdepth 1 -name "*.html" ! -name "index.html" | sort | while IFS= read -r html; do - name=$(basename "$html" .html) - printf '
  • %s
  • ' "$(basename "$html")" "$name" + for deck in "${DECKS[@]}"; do + href="${deck#*|}" + label="${href%/index.html}"; label="${label%.html}" + printf '
  • %s
  • ' "$href" "$label" done printf '
' printf '
Last built: %s · Marp
' "$(date -Iseconds)" diff --git a/docs/services/slides.md b/docs/services/slides.md index ea00c5b..5278ce3 100644 --- a/docs/services/slides.md +++ b/docs/services/slides.md @@ -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/.md` | `https://slides.makerfloss.eu/.html` | +| `slides//.md` | `https://slides.makerfloss.eu//.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. diff --git a/slides/tappaas/how-to/new-module/index.md b/slides/tappaas/how-to/new-module/index.md new file mode 100644 index 0000000..57779c9 --- /dev/null +++ b/slides/tappaas/how-to/new-module/index.md @@ -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 +--- + + + + + +# How to implement a TAPPaaS module + +From empty directory to merged pull request + + + +--- + +## 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// +├── 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 +├── .json # declaration: the module's contract +├── .nix # runtime definition +├── install.sh # create it +├── update.sh # move it forward +├── test.sh # prove it works +└── services// # per-service install/update/test/delete +``` + +--- + +## The path from nothing to merged + +```mermaid +flowchart LR + A[Scaffold from
00-Template] --> B[Declare
module.json] + B --> C[Define runtime
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/ +``` + +- Rename `template.json` → `.json`, `template.nix` → `.nix` +- Read `README-template.md`, then delete it +- `TODO: is there a scaffold script? if not, should there be?` + +--- + +## Step 2 — Declare the 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 + +`.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//install-service.sh +services//update-service.sh +services//test-service.sh +services//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?**